Introduction
\nContainerizing Go applications with Docker is the standard of modern development. However, basic Dockerfiles are often far from ideal: images become heavy, builds are slow, and in production, problems arise with logging, signals, and healthchecks. In this article, we will cover advanced techniques: from multi-stage builds to fine-tuning networks and volumes. You will learn how to make a Go application image minimal, secure, and production-ready.
\n\n1. Multi-stage builds: a minimal image in seconds
\nGo compiles into a static binary, which is ideal for containerization. However, if you use the full golang:1.22 image for running, you are dragging along the compiler, package manager, and system utilities. The solution is a multi-stage build.
Basic Dockerfile with multi-stage
\n# Stage 1: build\nFROM golang:1.22-alpine AS builder\nWORKDIR /app\nCOPY go.mod go.sum ./\nRUN go mod download\nCOPY . .\nRUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /app/main ./cmd/\n\n# Stage 2: minimal image\nFROM alpine:3.19\nRUN apk add --no-cache ca-certificates tzdata\nCOPY --from=builder /app/main /usr/local/bin/main\nEXPOSE 8080\nENTRYPOINT ["/usr/local/bin/main"]\n\nWhy does this work?
\n- \n
- Alpine — 5 MB, minimal set of utilities. \n
CGO_ENABLED=0— disables C dependencies, the binary becomes static. \n -ldflags="-s -w"— removes debug information, reducing the binary size by 30-40%. \n ca-certificatesandtzdata— for HTTPS requests and correct time. \n
Result: an image of 10-15 MB instead of 800+ MB. Build time — 15-20 seconds (with dependency caching).
\n\n2. Optimizing caching and layers
\nDocker caches layers. If you copy all the code before go mod download, any change in .go files invalidates the dependency cache. The correct order:
COPY go.mod go.sum ./\nRUN go mod download\nCOPY . .\n\nAlso use .dockerignore:
.git\n*.md\ntest/\ncoverage/\n.env\nDockerfile\n\nThis excludes unnecessary files from the build context, speeding up transfer to the Docker daemon.
\n\nAdvanced trick: caching modules
\nIf you use Go modules, you can mount the cache:
\nFROM golang:1.22-alpine AS builder\nWORKDIR /app\nRUN --mount=type=cache,target=/go/pkg/mod \\\n --mount=type=cache,target=/root/.cache/go-build \\\n go build -ldflags="-s -w" -o /app/main ./cmd/\n\nThis preserves downloaded modules and build artifacts between docker build runs.
3. Healthchecks, signals, and graceful shutdown
\nDocker can check the container's health. Add to the Dockerfile:
\n\nHEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \\\n CMD ["/usr/local/bin/main", "health"]\n\nIn the Go code, implement the /healthz endpoint:
package main\n\nimport (\n "net/http"\n "os"\n "os/signal"\n "syscall"\n)\n\nfunc main() {\n http.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {\n w.WriteHeader(http.StatusOK)\n w.Write([]byte("ok"))\n })\n\n srv := &http.Server{Addr: ":8080"}\n\n // Graceful shutdown\n go func() {\n sigint := make(chan os.Signal, 1)\n signal.Notify(sigint, syscall.SIGTERM, syscall.SIGINT)\n <-sigint\n srv.Shutdown(nil)\n }()\n\n srv.ListenAndServe()\n}\n\nImportant: The Go application must correctly handle SIGTERM (which Docker sends when stopping a container). Without graceful shutdown, you risk losing data or leaving connections open.