Learning paths / Containers / Containerize a real app

Recipe: containerize a Go app

Reading · 6 min · Module 8, lesson 4 of 641 min left in this module

Module 8 · Containerize a real appLesson 4 of 6

Goal: Containerize a Go app with a multi-stage, non-root Dockerfile and a .dockerignore, then prove it serves /healthz.

Key idea

Same checklist again. Go compiles to one binary that needs nothing else to run, so the build stage has the whole Go toolchain and the runtime stage has only the binary. That makes the smallest image of the three.

Get the app

git clone https://github.com/computesphere-samples/learn.git
cd learn/labs/containerize-go

A small app with / and /healthz, using only Go's standard library. Already cloned it? Run cd ../containerize-go from another sample's folder.

.dockerignore

server
.env*
.git
Dockerfile
.dockerignore

server is the binary go build makes if you built it locally. The image compiles its own.

Dockerfile

# Stage 1: compile a static binary with the full Go toolchain.
FROM golang:1.27-alpine AS build
WORKDIR /src
COPY go.mod ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -trimpath -ldflags "-s -w" -o /out/server .

# Stage 2: only the binary, on an image with no shell and no package manager.
FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=build /out/server /server
ENV PORT=3000
EXPOSE 3000
USER nonroot:nonroot
ENTRYPOINT ["/server"]

What each choice does:

  • go.mod is copied before the code, and go mod download fetches the dependencies in their own cached layer. This app has none, but the habit pays off in the next one that does. If it also has a go.sum, copy that too.
  • CGO_ENABLED=0 guarantees a static binary that needs no C libraries, so it runs on an image that has none.
  • -trimpath -ldflags "-s -w" leaves your folder paths and the debug symbols out of the binary, which makes it smaller.
  • distroless/static with the :nonroot tag contains little more than certificates and a non-root user. USER nonroot:nonroot says so out loud (lesson 3.4.3).

Build, run and check

docker build -t containerize-go:1.0.0 .
docker run -d --rm --name containerize-go -p 3000:3000 containerize-go:1.0.0
curl -i localhost:3000/healthz
HTTP/1.1 200 OK
Content-Type: application/json
...
{"status":"ok"}

Then check the user and the size, and stop it:

docker image inspect containerize-go:1.0.0 --format '{{.Config.User}}'
docker image ls containerize-go
docker stop containerize-go

The user is nonroot:nonroot, and the image is about 15 MB. The golang image it was built with is hundreds of MB; none of that ships.

Check yourself

The Go image is about 15 MB and the Node image from lesson 3.8.2 is over 200 MB. Where does the difference come from?