Skip to content

Expert · CLI tools and Go in cloud and DevOps · topic 41 of 46 · 35 min

Docker images for Go services

Explanation

A multi-stage Dockerfile builds your Go binary in the official golang image, then copies only the binary into a tiny final image such as distroless static or scratch. The result is small, starts fast, and has very little attack surface. Run it as a non-root user and pass configuration through environment variables.

Why containers, and why Go fits so well

A container image packages your program with everything it needs, so it runs the same on a laptop, in CI and in the cloud. Docker builds and runs images; Kubernetes runs many containers across servers. A Go service is a single static binary, so its image can be almost empty: no language runtime, no package manager, no shell.

A multi-stage Dockerfile

dockerfile
# ---- build stage ----
FROM golang:1.26 AS build
WORKDIR /src

# copy only module files first so the download layer is cached
COPY go.mod go.sum ./
RUN go mod download

COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags="-s -w" -o /out/api .

# ---- final stage ----
FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=build /out/api /api
USER nonroot:nonroot
EXPOSE 8080
ENTRYPOINT ["/api"]
Stage 1 has the full Go toolchain (hundreds of MB). Stage 2 has only your binary on a base of a few MB. Use the current Go and distroless tags when you build.
  • FROM golang:1.26 AS build: the official Go image from Docker Hub, named "build" for later.
  • Copying go.mod and go.sum first: Docker caches each step, so dependencies are re-downloaded only when those files change, not on every code edit.
  • CGO_ENABLED=0: a static binary that needs no C library, so it works on the minimal base.
  • distroless/static: Google's minimal base with CA certificates (for HTTPS calls), time-zone data and a non-root user, but no shell or package manager.
  • USER nonroot: if an attacker ever breaks in, they are not root inside the container.
  • ENTRYPOINT in JSON form: runs the binary directly as process 1, so it receives SIGTERM for graceful shutdown.

Building and running

bash
docker build -t expense-api:1.0.0 .
docker images expense-api          # see how small it is
docker run --rm -p 8080:8080 \
  -e DATABASE_URL="postgres://app:change-me@host.docker.internal:5432/app" \
  expense-api:1.0.0
curl localhost:8080/healthz
-p maps a host port to the container port; -e sets environment variables. The password here is a local example; real secrets come from your platform's secret store.

scratch versus distroless versus alpine

Base imageContainsNotes
scratchNothing at allSmallest. You must copy in CA certificates (and tzdata) yourself if you call HTTPS or load time zones. No user database, so set USER by number (USER 65532).
gcr.io/distroless/staticCA certs, tzdata, /etc/passwd, nonroot userA safe default for static Go binaries.
alpineA small Linux with a shell and apkHandy for debugging, but more to patch and scan.
golangFull Go toolchainFor the build stage only, never the final image.

.dockerignore

output
.git
bin/
dist/
*.env
.env*
coverage.out
Keep secrets, build output and the Git history out of the build context. Smaller context, faster builds, no leaked .env files.

Running Go well in containers

  • Listen on 0.0.0.0 (":8080"), not 127.0.0.1, or the port is unreachable from outside the container.
  • Log to stdout/stderr (slog to os.Stdout); the platform collects it.
  • Handle SIGTERM with graceful shutdown (module 8).
  • Memory: Go 1.19+ honours the GOMEMLIMIT environment variable, a soft memory limit for the garbage collector. Setting it a little below the container's memory limit helps avoid out-of-memory kills.
  • CPU: since Go 1.25, GOMAXPROCS on Linux follows the container's CPU limit automatically. Older versions used the host's CPU count, which some teams fixed with the go.uber.org/automaxprocs library.
Careful: No shell in distroless or scratch means docker exec -it ... sh will not work. That is a security feature. For debugging, use logs, a debug build stage, or tools such as kubectl debug with a separate debug image.
Tip: Scan images for known vulnerabilities in CI (for example with docker scout or trivy), and rebuild regularly so the Go toolchain and base image get security patches.

Need help?

Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.

Try:

Watch the lesson

Docker images for Go services · 1/7

Docker for Go

Containers package your program so it runs the same everywhere. Go makes very small images.

Prefer a video by a teacher? Find free videos on “golang multi stage dockerfile distroless scratch small image” on YouTube ↗

In real life: From 900 MB to about 15 MB

A startup's first Dockerfile used FROM golang and ran go run main.go inside the container. The image was close to a gigabyte, took minutes to pull on every deploy, ran as root, and shipped the compiler and source code to production. Switching to a multi-stage build with distroless brought it down to roughly the binary size plus a couple of MB, deploys became much faster, and the security scan went almost silent. (Exact sizes depend on your code and versions.)

dockerfile
# before (do not do this)
FROM golang:1.26
WORKDIR /app
COPY . .
CMD ["go", "run", "main.go"]

# after: see the multi-stage Dockerfile above
go run in a container compiles at every start and ships the whole toolchain.

Practice (0/4 done)

Task 1 · Question

Why use a multi-stage build for Go?

Task 2 · Question

Your Go service in a scratch image fails on HTTPS calls with a certificate error. Why?

Task 3 · Question

The server listens on 127.0.0.1:8080 inside the container. docker run -p 8080:8080 ... and curl from the host fails. Fix?

Task 4 · Practice

Write a multi-stage Dockerfile and a .dockerignore for your expense API. Build it, check the image size with docker images, run it with -p and -e, call /healthz with curl, then stop it with docker stop and confirm from the logs that graceful shutdown ran.

Quick quiz

Question 1

1.Why copy go.mod and go.sum before the rest of the source?

Question 2

2.What does distroless static NOT include?

Question 3

3.Why run the container as a non-root user?

Question 4

4.What does GOMEMLIMIT do?

Read more (free)

Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.

Why copy go.mod and go.sum before the rest of the source?

Answer: So the go mod download layer is cached and reused when only code changes

Layer caching skips re-downloading dependencies.

What does distroless static NOT include?

Answer: A shell

No shell or package manager, which reduces the attack surface.

Why run the container as a non-root user?

Answer: It limits damage if the process is compromised

Least privilege is a basic security practice.

What does GOMEMLIMIT do?

Answer: Gives the Go garbage collector a soft memory target

The GC works harder as memory approaches the limit.

What is the main benefit of a multi-stage build?Related

Answer: Build tools and source stay in earlier stages, so the final image is smaller and safer

Only what you COPY --from into the last stage ends up in the final image.

From Writing Dockerfiles: caching, multi-stage builds and small images

Why copy package*.json and run npm ci BEFORE COPY . . in a Dockerfile?Related

Answer: So Docker can reuse the installed packages when only the code changes

Docker caches each step. If package files did not change, the install step is reused and builds are fast.

From Docker: packaging your app

Why is mounting /var/run/docker.sock into a container dangerous?Related

Answer: It lets the container control the Docker daemon, and so effectively the whole host

With the socket, a container can start a new privileged container that mounts the host file system.

From Image and container security

What is the full reference for the image you get with docker pull python?Related

Answer: docker.io/library/python:latest

The default registry is docker.io, official images live under library/, and the default tag is latest.

From Containers, images and registries

Pass the quiz (60% or more) or mark this topic as done when you are ready.

Your progress is saved on this device only.

Next steps

  1. 1 more topic in “CLI tools and Go in cloud and DevOps” before its exam: Go in cloud and DevOps: Kubernetes, Terraform and Prometheus.
  2. Module 9 exam: CLI tools and Go in cloud and DevOps · 12 questions · 20 min, without help. 4 topics of this module still to finish first.
  3. Then the next module, Careers and interviews. 46 topics left in the course before the final exam.

← Previous: Cross-compiling and build flags