Back to Blog
Tutorial9 min read2026-07-02

How to Deploy a Go Gin API to Production

Go compiles to a single static binary, which makes it ideal for tiny production images. Here's how to build a minimal Docker image for a Gin API, add graceful shutdown, and wire in a managed database.

Ajay Kumar
Ajay Kumar
Founder & DevOps, PandaStack

Go's killer feature for deployment is the static binary: no runtime to install, no dependency hell, just one file. That means tiny container images and instant startup — perfect for scale-to-zero. This guide deploys a Gin API to production the lean way.

The minimal image: scratch or distroless

Because Go compiles to a static binary, your final image can be almost nothing. A multi-stage build compiles the binary, then copies it into a scratch or distroless image:

# --- build stage ---
FROM golang:1.23 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 ./cmd/server

# --- runtime stage ---
FROM gcr.io/distroless/static-debian12
COPY --from=build /app/server /server
EXPOSE 8080
USER nonroot:nonroot
ENTRYPOINT ["/server"]

Key flags:

  • CGO_ENABLED=0 produces a fully static binary with no libc dependency — required for scratch/distroless static.
  • -ldflags="-s -w" strips debug symbols, shrinking the binary.

Distroless static ships no shell and no package manager, dramatically reducing attack surface. Final images are often under 15 MB.

Reading config from the environment

Go's standard library is all you need:

func main() {
    port := os.Getenv("PORT")
    if port == "" {
        port = "8080"
    }
    dsn := os.Getenv("DATABASE_URL")
    // ...
}

Bind to 0.0.0.0 (Gin does by default with :port) so the container accepts external traffic.

Graceful shutdown

This is the part most Gin tutorials skip. When your platform sends SIGTERM during a rolling deploy, you want in-flight requests to finish:

srv := &http.Server{Addr: ":" + port, Handler: router}

go func() {
    if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
        log.Fatalf("listen: %s", err)
    }
}()

quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit

ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
    log.Fatalf("forced shutdown: %s", err)
}

Without this, a deploy can drop requests and leave connections half-open.

A managed database

Use pgx (the modern PostgreSQL driver) with a connection pool:

import "github.com/jackc/pgx/v5/pgxpool"

pool, err := pgxpool.New(context.Background(), os.Getenv("DATABASE_URL"))
if err != nil {
    log.Fatal(err)
}
defer pool.Close()

Configure pool.Config().MaxConns so total connections across replicas stay under your database's limit. For migrations, a tool like golang-migrate run as a deploy step keeps schema changes clean.

Health checks

router.GET("/healthz", func(c *gin.Context) {
    c.JSON(200, gin.H{"status": "ok"})
})
router.GET("/readyz", func(c *gin.Context) {
    if err := pool.Ping(c); err != nil {
        c.JSON(503, gin.H{"status": "db unavailable"})
        return
    }
    c.JSON(200, gin.H{"status": "ready"})
})

Set Gin to release mode

In production, set GIN_MODE=release to disable debug logging and the startup warning:

GIN_MODE=release

Deploying on PandaStack

  1. 1Create a PostgreSQL database — DATABASE_URL is injected automatically.
  2. 2Connect your repo as a container app; PandaStack detects Go and the Dockerfile and builds via rootless BuildKit (or buildpacks detect Go modules if you skip the Dockerfile).
  3. 3Set GIN_MODE=release and any secrets in the dashboard.
  4. 4Push — live build logs stream and you get automatic SSL.

Go's instant startup makes it an ideal fit for free-tier scale-to-zero on PandaStack: cold starts are fast because there's no runtime to warm up.

ConcernSetting
Image basedistroless/static or scratch
Build flagsCGO_ENABLED=0 -ldflags="-s -w"
ModeGIN_MODE=release
DB driverpgx with pool
Shutdownsrv.Shutdown(ctx) on SIGTERM

Common pitfalls

  • CGO_ENABLED=1 with a scratch image — the binary needs libc and crashes; keep CGO off or use a distroless base with glibc.
  • No graceful shutdown — deploys drop requests.
  • Debug mode in production — noisy logs, slight overhead.
  • Unbounded connection pool — exhausts the database under load.

References

  • Gin documentation: https://gin-gonic.com/docs/
  • Go distroless images: https://github.com/GoogleContainerTools/distroless
  • net/http graceful shutdown: https://pkg.go.dev/net/http#Server.Shutdown
  • pgx PostgreSQL driver: https://github.com/jackc/pgx
  • golang-migrate: https://github.com/golang-migrate/migrate

---

Go's tiny images and instant cold starts pair perfectly with PandaStack's free-tier scale-to-zero. Add a managed database with DATABASE_URL auto-wired and push your repo. Deploy 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