Dockerfile Refactoring
Refactor Dockerfiles safely by applying these patterns incrementally. Test builds after each change.
Pre-Refactor Checklist
- Verify current Dockerfile builds:
docker build -t test:before . - Note existing build time and image size
- Check for
.dockerignorefile - Identify the base image and its update frequency
Multi-Stage Builds
Split build dependencies from runtime. Only final stage artifacts ship.
# Build stage
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
# Runtime stage
FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY . .
CMD ["node", "server.js"]
Reusable stages: If multiple images share setup, create a common base stage.
Layer Cache Optimization
Order instructions from least to most frequently changing:
- Base image and system packages (rarely change)
- Language runtime dependencies (change occasionally)
- Application dependencies (change with lock files)
- Application code (changes most often)
Critical: Always combine apt-get update && apt-get install in one RUN:
# WRONG - cache issues
RUN apt-get update
RUN apt-get install -y curl
# CORRECT
RUN apt-get update && apt-get install -y --no-install-recommends \
curl \
&& rm -rf /var/lib/apt/lists/*
Instruction Patterns
FROM
- Use specific tags, not
latest:FROM python:3.12-slim - Pin digests for supply chain security:
FROM python:3.12-slim@sha256:abc123... - Prefer
-slimor-alpinevariants
RUN
- Chain commands with
&&to reduce layers - Sort multi-line package lists alphabetically
- Use
--no-install-recommendswith apt-get - Clean up in the same layer:
&& rm -rf /var/lib/apt/lists/* - Use heredocs for complex scripts:
RUN <<EOF
set -e
apt-get update
apt-get install -y --no-install-recommends curl
rm -rf /var/lib/apt/lists/*
EOF
COPY vs ADD
- Prefer
COPYfor local files - Use
ADDonly for: remote URLs with checksums, or auto-extracting tarballs - Use bind mounts for build-time-only files:
RUN --mount=type=bind,source=requirements.txt,target=/tmp/requirements.txt \
pip install -r /tmp/requirements.txt
ENV
- Environment variables persist across layers; use single RUN for secrets:
# WRONG - ADMIN_USER leaks to image
ENV ADMIN_USER=admin
RUN echo $ADMIN_USER > /config
# CORRECT - variable doesn't persist
RUN export ADMIN_USER=admin && echo $ADMIN_USER > /config
USER
- Create non-root user early, switch late:
RUN groupadd -r appuser && useradd --no-log-init -r -g appuser appuser
# ... install dependencies as root ...
USER appuser
CMD ["./app"]
ENTRYPOINT + CMD
ENTRYPOINTfor the command,CMDfor default arguments:
ENTRYPOINT ["python"]
CMD ["--help"]
WORKDIR
- Always use absolute paths
- Prefer
WORKDIR /appoverRUN cd /app && ...
EXPOSE
- Document ports but don't assume they're published
- Use standard ports (80, 443, 8080, etc.)
.dockerignore
Create if missing. Exclude:
.git
.gitignore
node_modules
__pycache__
*.pyc
.env
*.md
Dockerfile*
docker-compose*
.dockerignore
Common Refactoring Transforms
| Before | After | Why |
|---|---|---|
FROM ubuntu:latest |
FROM ubuntu:24.04 |
Reproducible builds |
Multiple RUN apt-get |
Single chained RUN |
Fewer layers, cache efficiency |
COPY . . early |
COPY . . last |
Better cache utilization |
ADD local.tar.gz /app |
COPY local.tar.gz /app + RUN tar |
Explicit, debuggable |
| Root user throughout | USER nonroot at end |
Security |
No .dockerignore |
Add .dockerignore |
Smaller context, faster builds |
Validation
After refactoring:
- Build:
docker build -t test:after . - Compare size:
docker images test - Run tests:
docker run test:after <test-command> - Check no secrets leaked:
docker history test:after
Safe Refactoring Order
- Add
.dockerignore(no build change) - Pin base image version (minimal risk)
- Reorder COPY statements (cache optimization)
- Combine RUN statements (reduce layers)
- Add multi-stage build (larger change, test thoroughly)
- Add non-root user (may need permission fixes)