Images & Dockerfile
Build optimized Docker images with Dockerfiles — understand layers, follow best practices, and minimize image size for faster deployments.
Writing a Dockerfile
A Dockerfile defines image build steps. FROM sets the base image. RUN executes commands during build. COPY/ADD transfer files. WORKDIR sets the working directory. CMD/ENTRYPOINT define the default process.
Each instruction creates a cached layer. Order instructions from least to most frequently changed — dependencies before application code — to maximize cache hits.
- Use specific tag pins: node:20-alpine, not node:latest
- COPY is preferred over ADD unless you need tar extraction or URL download
- CMD can be overridden at runtime; ENTRYPOINT cannot
FROM node:20-alpine WORKDIR /app COPY package*.json ./ RUN npm ci --only=production COPY . . EXPOSE 3000 CMD ["node", "server.js"]
Understanding Layers
Each Dockerfile instruction creates an immutable layer. Layers are shared across images — if two images use node:20-alpine, that base layer is stored once. Changing one layer invalidates cache for all subsequent layers.
Inspect layers with docker history. Smaller layers mean faster pulls and less storage. Combine RUN commands with && to reduce layer count.
# Bad: 3 layers
RUN apt-get update
RUN apt-get install -y curl
RUN rm -rf /var/lib/apt/lists/*
# Good: 1 layer
RUN apt-get update && apt-get install -y curl \
&& rm -rf /var/lib/apt/lists/*Multi-Stage Builds
Multi-stage builds use multiple FROM statements. The build stage compiles code with full toolchain. The production stage copies only artifacts into a minimal base image.
This pattern reduces a Node.js image from 1GB+ (with build tools) to under 150MB (alpine with compiled output). Go and Rust benefit even more dramatically.
# Build stage FROM node:20-alpine AS builder WORKDIR /app COPY package*.json ./ RUN npm ci COPY . . RUN npm run build # Production stage FROM node:20-alpine WORKDIR /app COPY --from=builder /app/dist ./dist COPY --from=builder /app/node_modules ./node_modules CMD ["node", "dist/server.js"]
Image Optimization
Choose minimal base images: alpine variants are 5MB vs 900MB for full images. Use .dockerignore to exclude node_modules, .git, and test files from the build context. Scan images for vulnerabilities with docker scout or trivy.
Distroless images contain only your application and runtime — no shell, no package manager. They are the most secure option for production.
- alpine images use musl libc — test compatibility with native modules
- Distroless: gcr.io/distroless/nodejs20-debian12
- Pin base image digests for reproducible builds
# .dockerignore node_modules .git *.md .env coverage dist
BuildKit and Build Commands
BuildKit is the modern build engine with parallel stage execution, cache mounts, and secret handling. Enable with DOCKER_BUILDKIT=1 or configure as default.
Use docker build -t name:tag . to build. Push with docker push after tagging for your registry. Buildx supports multi-platform builds for ARM and AMD from a single machine.
docker build -t myapp:1.0.0 . docker tag myapp:1.0.0 registry.example.com/myapp:1.0.0 docker push registry.example.com/myapp:1.0.0 # Multi-platform build docker buildx build --platform linux/amd64,linux/arm64 -t myapp:latest .