Writing Dockerfiles
Build custom images with FROM, RUN, COPY, CMD, ENTRYPOINT, and layer caching
Knowledge Debt detected
You can study this freely — but your score may plateau if these foundations have gaps. The Mastery badge requires them to be solid.
Explanation
A Dockerfile is a text file with instructions to build your own Docker image layer by layer.
Minimal Python app Dockerfile:
dockerfile # Base image — which OS and runtime to start from FROM python:3.12-slim # Set working directory inside the container WORKDIR /app # Copy dependency list first (for cache efficiency) COPY requirements.txt . # Install dependencies RUN pip install --no-cache-dir -r requirements.txt # Copy the rest of the source code COPY . . # What to run when the container starts CMD ["python", "main.py"]
Build it: docker build -t myapp:latest . Run it: docker run myapp:latest
Key instructions:
| Instruction | Purpose | |---|---| | FROM | Base image (always first) | | WORKDIR | Set current dir (creates if missing) | | COPY src dest | Copy files from host into image | | RUN cmd | Execute shell command during build | | ENV KEY=value | Set environment variable | | EXPOSE port | Document which port the app uses | | CMD ["arg"] | Default command when container starts | | ENTRYPOINT ["cmd"] | Fixed executable (CMD provides default args) |
CMD vs ENTRYPOINT:
dockerfile # CMD — can be overridden with docker run ... <command> CMD ["python", "main.py"] # docker run myapp python other.py → runs other.py # ENTRYPOINT — cannot be overridden (only args can change) ENTRYPOINT ["python"] CMD ["main.py"] # docker run myapp other.py → runs: python other.py
Layer caching — order matters:
dockerfile # WRONG: copies everything first — pip install runs on every code change COPY . . RUN pip install -r requirements.txt # RIGHT: copy requirements first, install, then copy code COPY requirements.txt . RUN pip install -r requirements.txt COPY . . # cache miss here on code changes — pip step is still cached `` Docker caches each layer. A cache miss invalidates all subsequent layers. Put slow, rarely-changing steps (installs) before fast, frequently-changing steps (source code).
Multi-stage builds — keep production images small:
dockerfile # Stage 1: builder (has compilers, dev tools) FROM python:3.12 AS builder WORKDIR /build COPY requirements.txt . RUN pip install --target=/install -r requirements.txt # Stage 2: runtime (slim, no build tools) FROM python:3.12-slim COPY --from=builder /install /usr/local/lib/python3.12/site-packages COPY . /app WORKDIR /app CMD ["python", "main.py"]
Always use .dockerignore:
# .dockerignore __pycache__/ *.pyc .git/ .env node_modules/ Without this, COPY . . sends your entire repo (including .git`) to the Docker build context.
Examples
Production-ready Dockerfile for a FastAPI app
Separating COPY requirements.txt / RUN pip from COPY . . means pip only reruns when requirements.txt changes, not on every code edit
# Dockerfile
FROM python:3.12-slim
WORKDIR /app
# Install deps separately for cache efficiency
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8000
# Use uvicorn as the ENTRYPOINT; CMD provides default args
ENTRYPOINT ["uvicorn", "main:app"]
CMD ["--host", "0.0.0.0", "--port", "8000"]
# .dockerignore
# __pycache__/
# .env
# .git/
# tests/Multi-stage build: Go app with tiny final image
Multi-stage builds are the idiomatic way to ship small production images; only the final stage is deployed
# Stage 1: compile
FROM golang:1.22 AS builder
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download # cache dep download separately
COPY . .
RUN CGO_ENABLED=0 go build -o /app ./cmd/server
# Stage 2: runtime — scratch has NO OS, just the binary
FROM scratch
COPY --from=builder /app /app
ENTRYPOINT ["/app"]
# Final image: ~5 MB vs ~900 MB for the builder stageHow well did you understand this?
Next in Docker & Containers
Docker Compose