Fiber is one of the most popular Go web frameworks, built on top of fasthttp with an Express-like API. Go's compile-to-static-binary model makes Fiber apps a joy to deploy: no runtime, no interpreter, just a binary that starts in milliseconds. This guide covers taking a Fiber API from go build to a production deployment with a database, a minimal container, and the operational details people skip.
A realistic Fiber app
Here's a small but complete Fiber service with a health check, a route, graceful shutdown, and a Postgres connection. The graceful-shutdown part is what separates a toy from something you can deploy behind a load balancer that does rolling restarts.
package main
import (
"context"
"log"
"os"
"os/signal"
"syscall"
"time"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/logger"
"github.com/gofiber/fiber/v2/middleware/recover"
"github.com/jackc/pgx/v5/pgxpool"
)
func main() {
pool, err := pgxpool.New(context.Background(), os.Getenv("DATABASE_URL"))
if err != nil {
log.Fatalf("db connect: %v", err)
}
defer pool.Close()
app := fiber.New(fiber.Config{
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
})
app.Use(recover.New())
app.Use(logger.New())
app.Get("/healthz", func(c *fiber.Ctx) error {
if err := pool.Ping(c.Context()); err != nil {
return c.SendStatus(fiber.StatusServiceUnavailable)
}
return c.SendString("ok")
})
app.Get("/users/:id", func(c *fiber.Ctx) error {
var name string
err := pool.QueryRow(c.Context(),
"SELECT name FROM users WHERE id=$1", c.Params("id")).Scan(&name)
if err != nil {
return c.Status(404).JSON(fiber.Map{"error": "not found"})
}
return c.JSON(fiber.Map{"name": name})
})
port := os.Getenv("PORT")
if port == "" {
port = "3000"
}
go func() {
if err := app.Listen(":" + port); err != nil {
log.Fatalf("listen: %v", err)
}
}()
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Println("shutting down...")
_ = app.ShutdownWithTimeout(10 * time.Second)
}Two things worth highlighting: the app reads PORT from the environment (platforms inject this), and it reads DATABASE_URL rather than hardcoding credentials. Both are the conventions almost every modern host expects.
A tiny production Dockerfile
Go's static binaries let you build absurdly small images. Use a multi-stage build and ship on a distroless or scratch base:
# --- build stage ---
FROM golang:1.23-alpine AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /app/server .
# --- run stage ---
FROM gcr.io/distroless/static-debian12
COPY --from=build /app/server /server
EXPOSE 3000
USER nonroot:nonroot
ENTRYPOINT ["/server"]CGO_ENABLED=0 is critical — it produces a fully static binary so you can use the minimal distroless static base. The -ldflags="-s -w" strips debug info, shaving a few MB. The result is typically a 10–20 MB image that cold-starts almost instantly.
Deploying
The deployment story is simple because the artifact is simple. On any platform that builds from a Dockerfile, push your repo and let it build. On PandaStack, you connect the Git repo and it detects the Dockerfile, builds the image with rootless BuildKit in an ephemeral Kubernetes Job, pushes to a registry, and Helm-deploys it — no host Docker socket involved. If you skip the Dockerfile entirely, buildpacks can auto-detect a Go module too, but for Go I prefer the explicit multi-stage Dockerfile above; it's smaller and you control the base image.
Key settings regardless of host:
- Health check path:
/healthz(the one that pings the DB) - Port: ensure the platform's expected port matches what your app listens on
- Env vars:
DATABASE_URL, plus any API keys
Wiring up the database
Fiber + pgx + Postgres is a common, fast stack. You need a Postgres instance and the connection string in DATABASE_URL. On PandaStack, provisioning a managed PostgreSQL (14.x or 16.x) and attaching it to the service auto-injects DATABASE_URL, so the exact code above runs unchanged — no copy-pasting connection strings between dashboards.
Run migrations as a separate step rather than on app boot. A common pattern is a cronjob or a one-off job using golang-migrate:
migrate -path ./migrations -database "$DATABASE_URL" upPerformance notes
Fiber is fast, but most real-world latency lives in the database and downstream calls, not the framework. A few things that actually move the needle:
- Connection pooling: pgxpool defaults are conservative. Tune
MaxConnsto your tier; too many connections against a small managed DB will exhaust its connection limit (free-tier DBs cap connections — check your plan). - Set timeouts:
ReadTimeout/WriteTimeouton the Fiber config prevent slow-client resource exhaustion. - Prefork is usually wrong on containers: Fiber's
Preforkmode forks the process per CPU. Inside a single-core container it just adds overhead. Leave it off unless you've benchmarked otherwise.
Scaling and cold starts
Go binaries cold-start fast, which makes them ideal for scale-to-zero setups. If you deploy on a free/preemptible tier with scale-to-zero, the first request after idle pays a brief cold-start, but because the binary is tiny and has no runtime warmup, that penalty is far smaller than for a JVM or Node app. For latency-sensitive APIs, run on a tier that keeps at least one instance warm.
References
- Fiber documentation: https://docs.gofiber.io/
- pgx (PostgreSQL driver): https://github.com/jackc/pgx
- Distroless base images: https://github.com/GoogleContainerTools/distroless
- golang-migrate: https://github.com/golang-migrate/migrate
- Go modules reference: https://go.dev/ref/mod
---
Got a Fiber API and a Dockerfile? PandaStack's free tier builds from your repo with rootless BuildKit, wires up a managed Postgres via DATABASE_URL, and gives you live build logs. Try it at https://dashboard.pandastack.io.