Skip to content

DevOps

Deploying a Go app with Docker: a small, safe image and a Compose file

Build a tiny multi-stage Docker image for a Go service, run it as a non-root user, add health checks, use Compose with PostgreSQL, and decide when Kubernetes is worth it.

By · Published · 3 min read

Short answer: build the binary in one Docker stage, copy only the binary into a minimal final image, run it as a non-root user and add a health check. The result is often 10 to 30 MB with few packages to attack. Run it with Compose next to PostgreSQL. You only need Kubernetes when you outgrow one server.

What does the Dockerfile look like?

# 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 -trimpath -ldflags="-s -w" -o /out/app ./cmd/app

# final stage
FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=build /out/app /app
USER nonroot:nonroot
EXPOSE 8080
ENTRYPOINT ["/app"]
  • CGO_ENABLED=0 produces a static binary with no C library dependency, so it runs on a minimal image.
  • Copying go.mod and go.sum first lets Docker cache downloaded modules, so rebuilds after code changes are fast.
  • -ldflags="-s -w" strips debug info and shrinks the binary.
  • A distroless static image has no shell and no package manager. There is little for an attacker to use if they get in. The nonroot tag runs as an unprivileged user.

If you need a shell for debugging, use alpine as the final stage instead and accept a slightly larger image. If you need CA certificates or timezone data, distroless static includes them.

How do you add a health check?

Distroless has no curl. Add a small subcommand to your binary, for example app healthcheck, that requests /healthz and exits 0 or 1, then call it in Compose.

services:
  api:
    build: .
    restart: unless-stopped
    ports:
      - "127.0.0.1:8080:8080"
    environment:
      DATABASE_URL: postgres://app:${DB_PASSWORD}@db:5432/app
    depends_on:
      db:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "/app", "healthcheck"]
      interval: 10s
      timeout: 3s
      retries: 5
    read_only: true
    cap_drop: [ALL]
    security_opt: ["no-new-privileges:true"]

  db:
    image: postgres:16
    restart: unless-stopped
    environment:
      POSTGRES_USER: app
      POSTGRES_PASSWORD: ${DB_PASSWORD}
      POSTGRES_DB: app
    volumes:
      - db_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app"]
      interval: 5s
      retries: 10

volumes:
  db_data:

The hardening options come from is Docker safe for production. The general file syntax is explained in Docker Compose in practice.

How do you handle configuration?

  • Read settings from environment variables, and fail at startup if a required one is missing. A clear message beats a mysterious runtime error.
  • Do not bake secrets into the image.
  • Log to standard output in a structured format. log/slog in the standard library does this.

How do you shut down cleanly?

Containers receive SIGTERM on stop. Handle it so in-flight requests finish.

ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()

srv := &http.Server{Addr: ":8080", Handler: mux}
go func() { _ = srv.ListenAndServe() }()

<-ctx.Done()
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
_ = srv.Shutdown(shutdownCtx)

How do migrations fit in?

Run migrations as a separate step with a different database role than the application's runtime role, so the running service cannot alter the schema. This is the two-role design from row-level security is not your only tenant boundary.

When do you need Kubernetes?

Not for one service and a database on one machine. Look at Kubernetes for beginners when you need multiple hosts, rolling updates across replicas, or automatic scaling. The same image works in both.

References

Author

Raktim Ranjit is a software engineer and the founder of NodeDR Infotech. He builds and maintains the software described here.

Have something in mind?

Let’s build something useful.

Tell me about the idea, product, or workflow you’re working through.

Tap to say hello