Fiber #
Fiber adalah framework web untuk Go yang terinspirasi dari Express.js. API-nya terasa familiar untuk developer Node.js — middleware, routing dengan app.Get("/path", handler), dan c.JSON() untuk response — tapi performanya setara Gin karena dibangun di atas fasthttp (bukan net/http standar). Untuk tim yang pindah dari Node.js ke Go, Fiber menawarkan ergonomics yang jauh lebih ramah dibanding Gin atau net/http standar.
Artikel ini membahas setup lengkap Docker Compose untuk local development Fiber, dari Dockerfile multi-stage, hot reload dengan Air, hingga integrasi database dan best practice. Setelah membaca artikel ini, kamu akan punya template yang bisa langsung dipakai untuk project Fiber baru.
Prasyarat #
Pastikan sudah terinstall:
- Docker dan Docker Compose versi terbaru
- Go 1.22+ (opsional, untuk development di host)
- Git
Struktur project Fiber standar:
my-fiber-app/
├── cmd/
│ └── server/
│ └── main.go
├── internal/
│ ├── handler/
│ │ └── user.go
│ ├── service/
│ │ └── user.go
│ ├── repository/
│ │ └── user.go
│ └── model/
│ └── user.go
├── go.mod
├── go.sum
├── Dockerfile
├── Dockerfile.dev
├── docker-compose.yml
├── .air.toml
├── .env
└── .dockerignore
Perbedaan Mendasar Fiber vs Gin #
Sebelum masuk ke teknis, pahami dulu kenapa Fiber berbeda dari Gin. Ini mempengaruhi banyak keputusan di Dockerfile dan Compose.
| Aspek | Gin | Fiber |
|---|---|---|
| HTTP engine | net/http standar |
fasthttp (custom) |
| API style | Express-like, idiomatic Go | Express-like, lebih dekat ke JS |
| Middleware | gin.HandlerFunc |
fiber.Handler (fungsi biasa) |
| Konteks | *gin.Context |
*fiber.Ctx (method-rich) |
| Body binding | c.ShouldBindJSON |
c.BodyParser |
| Static file | r.Static("/uploads", "./uploads") |
app.Static("/uploads", "./uploads") |
| Performa | Sangat cepat | Sedikit lebih cepat di benchmark sintetis |
Kompabilitas middleware net/http |
Ya | Tidak (perlu adapter) |
Karena Fiber pakai fasthttp (bukan net/http), middleware net/http standar tidak bisa dipakai langsung. Kamu perlu adapter github.com/gofiber/adaptor untuk pakai middleware seperti gorilla/handlers atau prometheus/client_golang. Untuk kebanyakan aplikasi hijau, hal ini bukan masalah — middleware Fiber sudah cukup lengkap.
Dockerfile Multi-Stage #
Pattern multi-stage sama dengan Go pada umumnya: builder stage untuk kompilasi, runtime stage ramping.
# syntax=docker/dockerfile:1.6
FROM golang:1.22-alpine AS builder
WORKDIR /app
# Install tools OS yang dibutuhkan saat build
RUN apk add --no-cache git ca-certificates
# Cache dependency
COPY go.mod go.sum ./
RUN go mod download
# Build binary statis
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /out/server ./cmd/server
# Stage kedua: runtime image ramping
FROM alpine:3.19
RUN apk add --no-cache ca-certificates tzdata
WORKDIR /app
# Non-root user
RUN addgroup -S app && adduser -S app -G app
USER app:app
COPY --from=builder /out/server /app/server
EXPOSE 3000
ENTRYPOINT ["/app/server"]
Image akhir < 20 MB. Cocok untuk di-deploy ke Cloud Run, Fly.io, atau Kubernetes.
Dockerfile untuk Development #
Untuk development dengan hot reload, pakai Air.
# Dockerfile.dev
FROM golang:1.22-alpine
WORKDIR /app
RUN apk add --no-cache git curl bash
# Install Air
RUN go install github.com/air-verse/air@latest
# Cache dependency
COPY go.mod go.sum ./
RUN go mod download
EXPOSE 3000
CMD ["air", "-c", ".air.toml"]
Konfigurasi Air (.air.toml):
root = "."
tmp_dir = "tmp"
[build]
bin = "./tmp/main"
cmd = "go build -o ./tmp/main ./cmd/server"
delay = 1000
exclude_dir = ["assets", "tmp", "vendor", "testdata"]
include_dir = ["cmd", "internal"]
include_ext = ["go", "tpl", "tmpl", "html", "yaml", "yml"]
exclude_regex = ["_test.go"]
exclude_unchanged = true
[log]
time = true
[misc]
clean_on_exit = true
docker-compose.yml #
# docker-compose.yml
services:
api:
build:
context: .
dockerfile: Dockerfile.dev
image: fiber-app:dev
container_name: fiber-api
ports:
- "3000:3000"
volumes:
- ./cmd:/app/cmd
- ./internal:/app/internal
- ./go.mod:/app/go.mod
- ./go.sum:/app/go.sum
- go-build:/app/tmp
environment:
- APP_ENV=development
- SERVER_PORT=3000
- DATABASE_URL=postgres://app:dev@db:5432/fiberapp?sslmode=disable
- REDIS_URL=redis://cache:6379/0
- JWT_SECRET=local-dev-secret-change-me
depends_on:
db:
condition: service_healthy
cache:
condition: service_healthy
command: air -c .air.toml
db:
image: postgres:16-alpine
container_name: fiber-db
environment:
- POSTGRES_USER=app
- POSTGRES_PASSWORD=dev
- POSTGRES_DB=fiberapp
volumes:
- db-data:/var/lib/postgresql/data
- ./db/init:/docker-entrypoint-initdb.d:ro
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app -d fiberapp"]
interval: 10s
timeout: 5s
retries: 5
ports:
- "5432:5432"
cache:
image: redis:7-alpine
container_name: fiber-cache
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 3
volumes:
- cache-data:/data
ports:
- "6379:6379"
volumes:
db-data:
cache-data:
go-build:
Contoh Aplikasi Fiber #
cmd/server/main.go:
package main
import (
"context"
"log"
"os"
"os/signal"
"syscall"
"time"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/cors"
"github.com/gofiber/fiber/v2/middleware/logger"
"github.com/gofiber/fiber/v2/middleware/recover"
"github.com/gofiber/fiber/v2/middleware/requestid"
"github.com/redis/go-redis/v9"
"gorm.io/driver/postgres"
"gorm.io/gorm"
"myapp/internal/handler"
"myapp/internal/model"
"myapp/internal/repository"
"myapp/internal/service"
)
func main() {
// Database dengan retry
var db *gorm.DB
var err error
dsn := os.Getenv("DATABASE_URL")
for i := 0; i < 30; i++ {
db, err = gorm.Open(postgres.Open(dsn), &gorm.Config{})
if err == nil {
break
}
log.Printf("waiting for db: %v", err)
time.Sleep(2 * time.Second)
}
if err != nil {
log.Fatal("db connection failed: ", err)
}
if err := db.AutoMigrate(&model.User{}); err != nil {
log.Fatal("migrate failed: ", err)
}
// Redis
rdb := redis.NewClient(&redis.Options{
Addr: os.Getenv("REDIS_URL"),
})
// Layers
userRepo := repository.NewUserRepository(db)
userSvc := service.NewUserService(userRepo)
userHandler := handler.NewUserHandler(userSvc)
// Fiber app
app := fiber.New(fiber.Config{
AppName: "my-fiber-app",
DisableStartupMessage: false,
ErrorHandler: func(c *fiber.Ctx, err error) error {
code := fiber.StatusInternalServerError
if e, ok := err.(*fiber.Error); ok {
code = e.Code
}
return c.Status(code).JSON(fiber.Map{"error": err.Error()})
},
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
})
// Middleware
app.Use(requestid.New())
app.Use(logger.New(logger.Config{
Format: "${time} ${status} ${method} ${path} ${latency} ${locals:requestid}\n",
TimeFormat: "2006-01-02T15:04:05Z07:00",
}))
app.Use(recover.New())
app.Use(cors.New(cors.Config{
AllowOrigins: "*",
AllowMethods: "GET,POST,PUT,DELETE,OPTIONS",
AllowHeaders: "Content-Type,Authorization,X-Request-ID",
}))
// Health
app.Get("/health", func(c *fiber.Ctx) error {
return c.JSON(fiber.Map{
"status": "ok",
"time": time.Now().Format(time.RFC3339),
})
})
// Routes
api := app.Group("/api/v1")
api.Post("/users", userHandler.Create)
api.Get("/users/:id", userHandler.Get)
// Start server dengan graceful shutdown
go func() {
if err := app.Listen(":3000"); err != nil {
log.Printf("server stopped: %v", err)
}
}()
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Println("shutting down...")
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := app.ShutdownWithContext(ctx); err != nil {
log.Fatal(err)
}
}
Handler (internal/handler/user.go):
package handler
import (
"errors"
"strconv"
"github.com/gofiber/fiber/v2"
"myapp/internal/service"
)
type UserHandler struct {
svc *service.UserService
}
func NewUserHandler(svc *service.UserService) *UserHandler {
return &UserHandler{svc: svc}
}
type createUserRequest struct {
Email string `json:"email"`
Name string `json:"name"`
Password string `json:"password"`
}
type errorResponse struct {
Error string `json:"error"`
}
func (h *UserHandler) Create(c *fiber.Ctx) error {
var req createUserRequest
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(errorResponse{Error: err.Error()})
}
if req.Email == "" || req.Name == "" || len(req.Password) < 8 {
return c.Status(fiber.StatusBadRequest).JSON(errorResponse{
Error: "email, name required and password min 8 chars",
})
}
u, err := h.svc.Register(c.UserContext(), req.Email, req.Name, req.Password)
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(errorResponse{Error: err.Error()})
}
return c.Status(fiber.StatusCreated).JSON(u)
}
func (h *UserHandler) Get(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 64)
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(errorResponse{Error: "invalid id"})
}
u, err := h.svc.Get(c.UserContext(), id)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(errorResponse{Error: err.Error()})
}
if u == nil {
return c.Status(fiber.StatusNotFound).JSON(errorResponse{Error: "user not found"})
}
return c.JSON(u)
}
Service (internal/service/user.go):
package service
import (
"context"
"errors"
"strings"
"golang.org/x/crypto/bcrypt"
"myapp/internal/model"
"myapp/internal/repository"
)
type UserService struct {
repo *repository.UserRepository
}
func NewUserService(repo *repository.UserRepository) *UserService {
return &UserService{repo: repo}
}
func (s *UserService) Register(ctx context.Context, email, name, password string) (*model.User, error) {
email = strings.ToLower(strings.TrimSpace(email))
existing, _ := s.repo.FindByEmail(ctx, email)
if existing != nil {
return nil, errors.New("email already registered")
}
hashed, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return nil, err
}
u := &model.User{
Email: email,
Name: strings.TrimSpace(name),
Password: string(hashed),
}
if err := s.repo.Create(ctx, u); err != nil {
return nil, err
}
return u, nil
}
func (s *UserService) Get(ctx context.Context, id uint64) (*model.User, error) {
return s.repo.FindByID(ctx, id)
}
Repository (internal/repository/user.go):
package repository
import (
"context"
"errors"
"gorm.io/gorm"
"myapp/internal/model"
)
type UserRepository struct {
db *gorm.DB
}
func NewUserRepository(db *gorm.DB) *UserRepository {
return &UserRepository{db: db}
}
func (r *UserRepository) Create(ctx context.Context, u *model.User) error {
return r.db.WithContext(ctx).Create(u).Error
}
func (r *UserRepository) FindByID(ctx context.Context, id uint64) (*model.User, error) {
var u model.User
err := r.db.WithContext(ctx).First(&u, id).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
return &u, err
}
func (r *UserRepository) FindByEmail(ctx context.Context, email string) (*model.User, error) {
var u model.User
err := r.db.WithContext(ctx).Where("email = ?", email).First(&u).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
return &u, err
}
Model (internal/model/user.go):
package model
import "time"
type User struct {
ID uint64 `gorm:"primaryKey" json:"id"`
Email string `gorm:"size:255;not null;uniqueIndex" json:"email"`
Name string `gorm:"size:255;not null" json:"name"`
Password string `gorm:"size:255;not null" json:"-"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
func (User) TableName() string {
return "users"
}
Middleware Bawaan Fiber #
Fiber punya banyak middleware resmi yang siap pakai:
| Middleware | Fungsi |
|---|---|
recover |
Tangkap panic, return 500 |
logger |
Log setiap request |
requestid |
Generate/extract request ID |
cors |
Handle CORS |
compress |
Gzip/deflate response |
limiter |
Rate limit |
jwt |
Validasi JWT |
basicauth |
HTTP Basic Auth |
cache |
HTTP cache header |
etag |
ETag header |
monitor |
Endpoint metrics (butuh ORM) |
csrf |
CSRF token |
helmet |
Security header |
healthcheck |
Liveness/readiness endpoint |
Contoh kombinasi middleware yang aman untuk API:
app.Use(requestid.New())
app.Use(recover.New())
app.Use(logger.New())
app.Use(cors.New())
app.Use(helmet.New())
app.Use(compress.New())
app.Use(etag.New())
helmet mengatur security header (X-Frame-Options, X-Content-Type-Options, dll). compress mengurangi ukuran response. etag mengaktifkan HTTP caching.
Validasi dengan go-playground/validator #
Sama seperti Gin, Fiber juga menggunakan go-playground/validator. Tapi tag-nya bukan binding, melainkan validate.
import "github.com/go-playground/validator/v10"
var validate = validator.New()
type createUserRequest struct {
Email string `json:"email" validate:"required,email"`
Name string `json:"name" validate:"required,min=2,max=100"`
Password string `json:"password" validate:"required,min=8,max=72"`
Roles []string `json:"roles" validate:"required,min=1,dive,oneof=admin user guest"`
Age int `json:"age" validate:"gte=18,lte=120"`
}
func (h *UserHandler) Create(c *fiber.Ctx) error {
var req createUserRequest
if err := c.BodyParser(&req); err != nil {
return c.Status(400).JSON(fiber.Map{"error": err.Error()})
}
if err := validate.Struct(req); err != nil {
return c.Status(400).JSON(fiber.Map{"error": err.Error()})
}
// lanjut
}
Build dan Run #
# Build image development
docker compose build
# Jalankan semua service
docker compose up -d
# Lihat log
docker compose logs -f api
# Shell ke dalam container
docker compose exec api sh
# Stop
docker compose down
# Reset total (hapus volume)
docker compose down -v
Akses:
- API: http://localhost:3000
- Health check: http://localhost:3000/health
- PostgreSQL:
localhost:5432 - Redis:
localhost:6379
Test:
# Register
curl -X POST http://localhost:3000/api/v1/users \
-H "Content-Type: application/json" \
-d '{"email":"[email protected]","name":"Siti","password":"password123"}'
# Get user
curl http://localhost:3000/api/v1/users/1
WebSocket dengan Fiber #
Fiber punya support WebSocket built-in lewat middleware github.com/gofiber/contrib/websocket.
import "github.com/gofiber/contrib/websocket"
app.Get("/ws", websocket.New(func(c *websocket.Conn) {
defer c.Close()
for {
mt, msg, err := c.ReadMessage()
if err != nil {
return
}
log.Printf("recv: %s", msg)
if err := c.WriteMessage(mt, msg); err != nil {
return
}
}
}))
Untuk real-time chat atau notification, WebSocket di Fiber sangat mudah di-setup. Alternatifnya pakai Server-Sent Events (SSE) untuk one-way communication.
File Upload dan Static File #
// Single file upload
app.Post("/upload", func(c *fiber.Ctx) error {
file, err := c.FormFile("file")
if err != nil {
return c.Status(400).JSON(fiber.Map{"error": err.Error()})
}
if file.Size > 5*1024*1024 {
return c.Status(413).JSON(fiber.Map{"error": "file too large"})
}
filename := fmt.Sprintf("./uploads/%d_%s", time.Now().UnixNano(), file.Filename)
if err := c.SaveFile(file, filename); err != nil {
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
}
return c.JSON(fiber.Map{"filename": filename, "size": file.Size})
})
// Serve static
app.Static("/uploads", "./uploads")
app.Static("/", "./public")
Testing dengan Testcontainers #
Sama dengan Gin, Fiber cocok dengan Testcontainers untuk integration test.
package handler_test
import (
"bytes"
"encoding/json"
"io"
"net/http/httptest"
"testing"
"time"
"github.com/gofiber/fiber/v2"
"github.com/stretchr/testify/assert"
"github.com/testcontainers/testcontainers-go/modules/postgres"
"gorm.io/driver/postgres"
"gorm.io/gorm"
"myapp/internal/handler"
"myapp/internal/model"
"myapp/internal/repository"
"myapp/internal/service"
"context"
)
func setupApp(t *testing.T) (*fiber.App, func()) {
ctx := context.Background()
pgC, err := postgres.RunContainer(ctx,
postgres.WithDatabase("test"),
postgres.WithUsername("test"),
postgres.WithPassword("test"),
)
if err != nil {
t.Fatal(err)
}
dsn, _ := pgC.ConnectionString(ctx, "sslmode=disable")
db, _ := gorm.Open(postgres.Open(dsn), &gorm.Config{})
db.AutoMigrate(&model.User{})
repo := repository.NewUserRepository(db)
svc := service.NewUserService(repo)
h := handler.NewUserHandler(svc)
app := fiber.New()
app.Post("/users", h.Create)
app.Get("/users/:id", h.Get)
cleanup := func() {
pgC.Terminate(ctx)
}
return app, cleanup
}
func TestCreateUser(t *testing.T) {
app, cleanup := setupApp(t)
defer cleanup()
body := map[string]string{
"email": "[email protected]",
"name": "Test",
"password": "password123",
}
b, _ := json.Marshal(body)
req := httptest.NewRequest("POST", "/users", bytes.NewReader(b))
req.Header.Set("Content-Type", "application/json")
resp, err := app.Test(req, -1)
assert.NoError(t, err)
assert.Equal(t, 201, resp.StatusCode)
bodyBytes, _ := io.ReadAll(resp.Body)
assert.Contains(t, string(bodyBytes), "[email protected]")
}
app.Test() menjalankan handler di-memory tanpa perlu listen ke port — sangat cepat untuk test.
Kapan Fiber Tepat, Kapan Tidak #
Pakai Fiber jika:
✓ Developer familiar dengan Express.js
✓ Butuh WebSocket built-in
✓ Suka API yang lebih deklaratif
✓ Microservice dengan throughput tinggi
Hindari Fiber jika:
✗ Butuh middleware net/http standar
✗ Library ekosistem (mis. OpenTelemetry SDK) wajib pakai net/http
✗ Tim kuat di Go idiomatic dan lebih suka net/http
✗ gRPC service
Karena Fiber pakai fasthttp, ada beberapa library Go yang tidak kompatibel. Contoh: prometheus/client_golang (untuk metrics), otelhttp (untuk tracing), dan beberapa middleware observability. Untuk pakai library tersebut, kamu perlu adapter atau implementasi custom.
Best Practice #
Pakai Air untuk Hot Reload #
Tanpa hot reload, Go terasa lambat untuk iterasi. Air memantau file .go dan rebuild otomatis. Pasang via go install di Dockerfile.dev.
Cache Go Module #
Selalu copy go.mod + go.sum lebih dulu, jalankan go mod download. Layer dependency jarang berubah, jadi cache lebih sering hit.
Bind Mount Source Code #
Di development, mount source code ke container. Volume go-build cache hasil build agar restart container lebih cepat.
Healthcheck dan Retry Loop #
PostgreSQL dan Redis harus punya healthcheck. Service Fiber tidak boleh start sebelum db dan cache healthy. Tambahkan retry loop di kode Go untuk koneksi awal.
Pisahkan Config dari Code #
Pakai os.Getenv() untuk semua konfigurasi. Sediakan .env.example di repo. Jangan hardcode URL, port, atau secret.
Pakai fasthttp dengan Sadar #
Fiber pakai fasthttp — ia tidak kompatibel dengan library net/http standar tanpa adaptor. Untuk metrics, tracing, dan middleware ekosistem, cek dulu apakah ada adapter Fiber-nya.
Graceful Shutdown #
Tambahkan signal handler agar server bisa shutdown dengan bersih saat container di-restart. Tanpa ini, koneksi aktif bisa terputus.
Troubleshooting #
Air Tidak Detect Perubahan #
Pastikan bind mount include direktori cmd/ dan internal/. Di Mac/Windows dengan Docker Desktop, kadang butuh beberapa detik. Naikkan delay di .air.toml ke 2000-3000 ms kalau rebuild terlalu sering.
Database Connection Refused #
Pakai depends_on: condition: service_healthy di Compose, dan tambahkan retry loop di kode Go. depends_on saja tidak cukup karena Docker hanya menunggu container start, bukan service siap.
Middleware net/http Tidak Berfungsi #
Fiber pakai fasthttp, jadi middleware net/http standar tidak bisa dipakai langsung. Cari versi Fiber-nya di github.com/gofiber/contrib atau tulis custom.
Port 3000 Sudah Dipakai #
lsof -i :3000
# atau
netstat -ano | findstr :3000
Stop proses atau ubah mapping di Compose ("3001:3000").
Memory Leak #
Pastikan setiap request yang allocate resource besar di-close dengan benar. Untuk koneksi database, pakai connection pool (sql.DB untuk database/sql, atau konfigurasi pool di GORM).
Ringkasan #
- Fiber ideal untuk developer Node.js yang pindah ke Go — API Express-like, ergonomics familiar, performa setara Gin.
- Fiber pakai
fasthttp, bukannet/http. Ini membuat middleware ekosistem Go standar tidak langsung kompatibel. Untuk observability dan metrics, perlu adapter atau implementasi custom.- Dockerfile multi-stage untuk production: builder stage dengan Go toolchain, runtime stage ramping dengan alpine. Image akhir < 20 MB.
- Dockerfile.dev untuk development: full toolchain + Air untuk hot reload.
- Hot reload dengan Air: pantau file
.go, rebuild, restart. Konfigurasi lewat.air.toml.- Layer cache Go: copy manifest dulu, baru source code. Build inkremental super cepat.
- Middleware built-in: recover, logger, requestid, cors, compress, etag, helmet, jwt, limiter. Tinggal
app.Use().- WebSocket built-in lewat
github.com/gofiber/contrib/websocket— sangat mudah untuk real-time app.- File upload lewat
c.FormFile()danc.SaveFile(). Static file lewatapp.Static().- Validasi dengan
go-playground/validatorlewat tagvalidate. Library validator terpisah, bukan built-in.- Healthcheck untuk service dependent: Postgres
pg_isready, Redisredis-cli ping. Pakaidepends_on: condition: service_healthydi Compose.- Testcontainers untuk integration test: spin up Postgres container, jalankan test, cleanup otomatis.
- Pakai Fiber jika familiar dengan Express.js, butuh WebSocket, atau suka API deklaratif. Hindari jika ekosistem
net/httpadalah hard requirement.- Best practice: pisahkan Dockerfile dev & prod, cache dependency, bind mount source, healthcheck, retry loop, graceful shutdown, sadar kompatibilitas fasthttp.
- Alternatif: Gin untuk kompatibilitas
net/http, chi untuk router ringan, net/http standar untuk service minimal.