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=0produces a fully static binary with no libc dependency — required forscratch/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=releaseDeploying on PandaStack
- 1Create a PostgreSQL database —
DATABASE_URLis injected automatically. - 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).
- 3Set
GIN_MODE=releaseand any secrets in the dashboard. - 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.
| Concern | Setting |
|---|---|
| Image base | distroless/static or scratch |
| Build flags | CGO_ENABLED=0 -ldflags="-s -w" |
| Mode | GIN_MODE=release |
| DB driver | pgx with pool |
| Shutdown | srv.Shutdown(ctx) on SIGTERM |
Common pitfalls
CGO_ENABLED=1with ascratchimage — 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