Expert · CLI tools and Go in cloud and DevOps · topic 41 of 46 · 35 min
Docker images for Go services
Explanation
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
# ---- 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"]- 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
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/healthzscratch versus distroless versus alpine
| Base image | Contains | Notes |
|---|---|---|
| scratch | Nothing at all | Smallest. 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/static | CA certs, tzdata, /etc/passwd, nonroot user | A safe default for static Go binaries. |
| alpine | A small Linux with a shell and apk | Handy for debugging, but more to patch and scan. |
| golang | Full Go toolchain | For the build stage only, never the final image. |
.dockerignore
.git
bin/
dist/
*.env
.env*
coverage.outRunning 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.
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
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.)
# before (do not do this)
FROM golang:1.26
WORKDIR /app
COPY . .
CMD ["go", "run", "main.go"]
# after: see the multi-stage Dockerfile abovePractice (0/4 done)
Why use a multi-stage build for Go?
Your Go service in a scratch image fails on HTTPS calls with a certificate error. Why?
The server listens on 127.0.0.1:8080 inside the container. docker run -p 8080:8080 ... and curl from the host fails. Fix?
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
Read more (free)
Related help
Before this, learn
This topic builds on these. If something here feels new, open them first.
Related topics
- Go in cloud and DevOps: Kubernetes, Terraform and PrometheusCLI tools and Go in cloud and DevOps · same module
- Command-line tools with flag and osCLI tools and Go in cloud and DevOps · same module
- Writing Dockerfiles: caching, multi-stage builds and small imagesDevOps: Git, Docker, Kubernetes and CI/CD · Docker
- Docker: packaging your appBackend Development · Deployment and scaling
- Image and container securityDevOps: Git, Docker, Kubernetes and CI/CD · Docker
- Containers, images and registriesDevOps: Git, Docker, Kubernetes and CI/CD · Docker
Questions people ask about this
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.
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.
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.
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 more topic in “CLI tools and Go in cloud and DevOps” before its exam: Go in cloud and DevOps: Kubernetes, Terraform and Prometheus.
- 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.
- Then the next module, Careers and interviews. 46 topics left in the course before the final exam.