Stage 4 · Provision
Building Images
Distroless Images
Use scratch, Alpine, and Google distroless bases while handling certificates, users, and debugging tradeoffs.
Base Image Options
The base image you choose determines your container's size, security surface, and debugging capabilities. There is a spectrum from full distributions (ubuntu, debian) to minimal bases (alpine, distroless) to empty (scratch). Each tradeoff affects image size, attack surface, and developer experience.
| Base | Size | Shell | Package Manager | Use Case |
|---|---|---|---|---|
| ubuntu | ~78MB | Yes | apt | Compatibility |
| alpine | ~7MB | Yes | apk | Development/small images |
| distroless | ~20MB | No | No | Production |
| scratch | 0MB | No | No | Static binaries |
scratch
scratch is the empty base image. It contains nothing — no shell, no libraries, no files. Your application binary and any required files are the only contents. This produces the smallest possible image and eliminates all unnecessary attack surface.
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY . .
RUN CGO_ENABLED=0 go build -o server .
FROM scratch
COPY --from=builder /app/server /server
ENTRYPOINT ["/server"]This produces a 10-15MB image containing only the Go binary. There is no shell, so docker exec sh does not work. Debugging requires copying a static binary like busybox into the image.
Alpine Linux
Alpine Linux is a security-oriented, lightweight distribution based on musl libc and BusyBox. At about 7MB, it provides a real shell, apk package manager, and a familiar Linux environment. It is the most common base for production containers that need shell access.
FROM node:20-alpine
RUN apk add --no-cache tini
WORKDIR /app
COPY package*.json ./
RUN npm ci --production
COPY . .
USER node
ENTRYPOINT ["/sbin/tini", "--"]
CMD ["node", "server.js"]Alpine gives you a shell, tini for PID 1 handling, and a package manager for system dependencies. The image is about 180MB with Node.js — small enough for fast pulls but large enough for easy debugging.
Alpine uses musl libc instead of glibc. Some pre-compiled binaries (like Python wheels or native Node.js modules) are built for glibc and may not work on Alpine. Test thoroughly before using Alpine as a base.
Google Distroless
Google's distroless images contain only the application runtime and its dependencies — no shell, no package manager, no OS utilities. They are based on Debian but stripped to the minimum. They include glibc, so binaries compiled on Debian/Ubuntu work without modification.
FROM gcr.io/distroless/java21-debian12
COPY target/app.jar /app.jar
ENTRYPOINT ["java", "-jar", "/app.jar"]
# For Node.js
FROM gcr.io/distroless/nodejs20-debian12
COPY dist/ /app/dist
CMD ["dist/server.js"]Distroless images provide glibc compatibility without the attack surface of a full OS. They are larger than Alpine but more compatible. Google maintains them for Java, Python, Node.js, and Go.
Certificates and Users
When using scratch or distroless, you often need to copy TLS certificates for HTTPS connections and set up non-root users. These files must be explicitly copied into the image since the base does not include them.
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY . .
RUN CGO_ENABLED=0 go build -o server .
FROM scratch
# CA certificates for HTTPS
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
# Non-root user
COPY --from=builder /etc/passwd /etc/passwd
COPY --from=builder /app/server /server
USER 1000
ENTRYPOINT ["/server"]Without CA certificates, your Go binary cannot make HTTPS requests. Without /etc/passwd, USER 1000 shows as a numeric ID in logs. Copy these files from the builder stage.
Debugging Tradeoffs
Minimal images make debugging harder. You cannot docker exec sh into a scratch container. Solutions include building debug variants with a shell, using ephemeral debug containers, or copying static debugging tools into the image.
# Production image
FROM scratch
COPY --from=builder /app/server /server
ENTRYPOINT ["/server"]
# Debug variant
FROM busybox
COPY --from=builder /app/server /server
COPY --from=builder /etc/ssl/certs /etc/ssl/certs
ENTRYPOINT ["/server"]Build both variants. Use the production image in production and the debug image during development. Tag them differently: myapp:1.0 and myapp:1.0-debug.
In Kubernetes, use ephemeral debug containers: kubectl debug -it mypod --image=busybox --target=mycontainer. This attaches a shell to a running container without modifying the production image.
Mark this lesson complete to store local progress and unlock a cleaner resume path the next time you visit.