Skip to main content

Docker Ep 2: Writing Production Dockerfiles

Rachmat Hidayat
Author
Rachmat Hidayat
Learn & sharing insights on TypeScript, Go, Kubernetes, DevOps, DevSecOps, SRE, Platform Engineering, AI/ML Engineering, and MLOps.
docker - This article is part of a series.
Part 2: This Article
Shipping a 1GB Docker image containing compilers, debuggers, and build tools to production is a security nightmare. Multi-Stage Builds allow you to separate the build environment from the tiny runtime container.

TL;DR (Quick Summary)
#

  • Multi-Stage Builds: Use multiple FROM statements to build binaries in a heavy stage and copy ONLY the compiled output to a lightweight scratch/distroless stage.
  • Layer Caching: Order Dockerfile directives from least frequently changed (COPY package.json) to most frequently changed (COPY . .).
  • Non-Root Security: Never run containers as USER root. Always explicitly declare USER node or USER 10001.
  • .dockerignore: Exclude .git, node_modules, and temporary logs from the build context.

1. Multi-Stage Build Architecture
#


graph LR
    subgraph Build Stage (golang:1.21-alpine)
        Source["Go Source Code"] --> Compiler["go build -o server"]
        Compiler --> Binary["Compiled Static Binary"]
    end

    subgraph Production Runtime (gcr.io/distroless/static-debian12)
        Binary -->|COPY --from=builder| CleanContainer["15MB Distroless Container"]
    end

2. Comparison: Naive vs Production Dockerfiles
#

Metric / FeatureNaive DockerfileProduction Multi-Stage Dockerfile
Image Size~1.2 GB (Includes Node/Go/Python toolchains).~20 MB (Runtime binary/script only).
Security SurfaceHigh (Shell, package managers, compilers present).Minimal (No bash, no package manager, non-root user).
Build TimeSlow (Rebuilds dependencies on every code edit).Blazing Fast (Leverages Docker layer cache).

3. Step-by-Step Lab: Building a Production Go Microservice Image
#

Let’s write a production-grade multi-stage Dockerfile for a Go API server.

The Production Dockerfile
#

Create Dockerfile:

# ==========================================
# Stage 1: Build Environment
# ==========================================
FROM golang:1.21-alpine AS builder

# Set working directory
WORKDIR /app

# Install build dependencies
RUN apk add --no-cache git

# Copy dependency manifests first for layer caching
COPY go.mod go.sum ./
RUN go mod download

# Copy source code and compile static binary
COPY . .
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \
    -ldflags="-w -s" \
    -o /app/server .

# ==========================================
# Stage 2: Minimal Distroless Production Runtime
# ==========================================
FROM gcr.io/distroless/static-debian12:nonroot

WORKDIR /

# Copy only the compiled binary from Stage 1
COPY --from=builder /app/server /server

# Expose port
EXPOSE 8080

# Enforce non-root user execution
USER nonroot:nonroot

ENTRYPOINT ["/server"]

Building and Verifying Image Sizes
#

Build the image:

docker build -t go-api:prod .

Compare image sizes:

docker images | grep go-api

Expected Terminal Output:

REPOSITORY   TAG       IMAGE ID       CREATED         SIZE
go-api       prod      a1b2c3d4e5f6   12 seconds ago   14.2MB

The production image is an incredible 14.2 MB!


4. Layer Caching Rules
#

Docker caches each instruction (RUN, COPY, ADD) as a read-only layer. If a layer changes, all subsequent layers are invalidated!

# 🔴 Bad Ordering: Invalidates npm install cache on EVERY file edit!
COPY . .
RUN npm install

# 🟢 Good Ordering: npm install is cached UNLESS package.json changes!
COPY package*.json ./
RUN npm install
COPY . .

5. Troubleshooting & Common Errors
#

Error 1: exec user process caused: no such file or directory
#

The Cause: You compiled a CGO/Go binary dynamically, and the scratch/distroless runtime image lacks the required C C-library (glibc). The Fix: Disable CGO during compilation: CGO_ENABLED=0 GOOS=linux go build ....


Summary & Next Steps
#

In this episode:

  • We reduced container image sizes from 1GB to 14MB using Multi-Stage Builds.
  • We optimized layer cache invalidation.
  • We secured containers using Distroless images and non-root execution.

Next, we move to Episode 3: Docker Networking Deep Dive!

docker - This article is part of a series.
Part 2: This Article