Back to Blog
Tutorial9 min read2026-06-29

How to Deploy a Go Fiber API

A complete walkthrough for shipping a Go Fiber API to production with a small static binary, a tiny multi-stage Docker image, graceful shutdown, and a managed Postgres backend.

Ajay Kumar
Ajay Kumar
Founder & DevOps, PandaStack

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" up

Performance 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 MaxConns to 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/WriteTimeout on the Fiber config prevent slow-client resource exhaustion.
  • Prefork is usually wrong on containers: Fiber's Prefork mode 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.

Ready to deploy?

Start free on PandaStack.

Start free on PandaStack

More in Tutorial

Browse all Tutorial articles →

See also