Learning paths / Containers / Writing Dockerfiles

Try it: shrink an image from 1.4 GB to 20 MB

Reading · 25 min · Module 4, lesson 5 of 630 min left in this module

Module 4 · Writing DockerfilesLesson 5 of 6

Goal: Rewrite a naive Dockerfile as a multi-stage build on a distroless base, and measure the difference.

You need Docker running (set up in lesson 3.1.1), git, curl, and a terminal.

Already have the Learn repository? Reading the size columns

If you cloned it in Application foundations, run git pull inside learn instead of cloning again, then cd labs/bloated-app.

Docker 29 lists two sizes. DISK USAGE is the image unpacked on your machine; CONTENT SIZE is the compressed download a server pulls. Older Docker versions show one SIZE column, the unpacked size. Your numbers can differ a little from these by processor type and Docker version.

  1. Build the naive image

    git clone https://github.com/computesphere-samples/learn.git
    cd learn/labs/bloated-app
    docker build -t bloated-app .
    

    Read the Dockerfile while it builds. Its comments point at each problem. transferring context is the folder being sent to the builder (lesson 3.4.4): every file in it, README.md included.

    You should seea line with transferring context: 2.70kB near the top, and the build ending with naming to docker.io/library/bloated-app:latest

  2. Measure it and look inside

    docker images bloated-app
    docker run --rm bloated-app ls
    docker run --rm bloated-app whoami
    

    The program itself, server, is a few megabytes. The rest is the Go toolchain, a full Debian, and your source code, running as root.

    You should seebloated-app:latest at 1.44GB DISK USAGE, then Dockerfile, README.md, go.mod, main.go and server, then root.

  3. Keep junk out of the build context

    Create .dockerignore with:

    .git
    .env
    *.md
    

    You should seea new .dockerignore file next to the Dockerfile.

  4. Write a multi-stage Dockerfile

    Create Dockerfile.slim, keeping the original for comparison:

    1. Stage 1, named build, from golang:1.27. Copy go.mod and the .go files, then build with CGO_ENABLED=0 into /out/server.
    2. Stage 2 from gcr.io/distroless/static-debian12:nonroot. Copy /out/server from the build stage.
    3. Run as nonroot:nonroot, and start /server. The app listens on 3000.
    If you're stuck
    # Stage 1: build with the full Go toolchain
    FROM golang:1.27 AS build
    WORKDIR /src
    COPY go.mod ./
    COPY *.go ./
    RUN CGO_ENABLED=0 go build -o /out/server .
    
    # Stage 2: ship only the binary, on a minimal base, as a non-root user
    FROM gcr.io/distroless/static-debian12:nonroot
    COPY --from=build /out/server /server
    EXPOSE 3000
    USER nonroot:nonroot
    ENTRYPOINT ["/server"]
    

    ENTRYPOINT works like CMD here. Distroless images have no shell, so use the JSON-array form of either. Without CGO_ENABLED=0, the container fails with exec /server: no such file or directory.

    You should seea new file, Dockerfile.slim, with two FROM lines.

  5. Build it and measure again

    docker build -f Dockerfile.slim -t bloated-app:slim .
    docker images bloated-app
    

    -f picks the Dockerfile; the . is still the build context. The second build is quick, because Docker already has golang:1.27 from step 1. Its transferring context line is smaller: .dockerignore kept README.md on your machine.

    Check yourself

    The slim image is about 70 times smaller. Where did the Go compiler go?

    You should seebloated-app:slim at 20.2MB DISK USAGE, beside bloated-app:latest at 1.44GB.

  6. Run it and check it's healthy

    docker run -d --name slim -p 3000:3000 bloated-app:slim
    curl localhost:3000/healthz
    docker image inspect bloated-app:slim --format '{{.Config.User}}'
    

    Same app, same answer, no root.

    You should see{"status":"ok"}, then nonroot:nonroot.

  7. Clean up

    docker rm -f slim
    docker rmi bloated-app bloated-app:slim
    

    Docker also keeps a build cache, which makes rebuilds fast and takes disk space. docker builder prune clears it; that's optional, and the next build is slower for it.

    You should seeslim printed back, then both tags untagged and deleted.