A Dockerfile decides image size (which decides cold-start time, registry cost), build speed (CI minutes), and security (CVE surface). Get it right; this post is how.
Multi-stage builds
# Build stage
FROM rust:1.85 AS builder
WORKDIR /build
COPY Cargo.toml Cargo.lock ./
COPY src ./src
RUN cargo build --release
# Runtime stage
FROM gcr.io/distroless/cc:nonroot
COPY --from=builder /build/target/release/api /usr/local/bin/api
ENTRYPOINT ["api"]
Build with full toolchain. Runtime with just the binary. Result: ~30 MB image instead of 1.2 GB.
For language specifics: Production HTTP Service in Rust , Modern TypeScript Backend with Hono on Bun , FastAPI + Pydantic v2 + SQLAlchemy 2.0 .
Layer caching
# Bad — code change invalidates dep cache
COPY . .
RUN uv sync
# Good — deps cached separately from code
COPY pyproject.toml uv.lock ./
RUN uv sync --frozen
COPY . .
Order operations from least-changing to most. Deps change rarely; code changes per commit. Caching deps saves CI time.
BuildKit cache mounts
# syntax=docker/dockerfile:1.7
FROM python:3.13-slim
RUN --mount=type=cache,target=/root/.cache/pip \
pip install -r requirements.txt
--mount=type=cache persists /root/.cache/pip across builds even when the layer reruns. Massively faster.
For Rust:
RUN --mount=type=cache,target=/usr/local/cargo/registry \
--mount=type=cache,target=/build/target \
cargo build --release
For Bun:
RUN --mount=type=cache,target=/root/.bun/install/cache \
bun install --frozen-lockfile
Non-root user
FROM gcr.io/distroless/cc:nonroot
USER nonroot:nonroot # already the default in distroless:nonroot
Or:
FROM debian:bookworm-slim
RUN useradd -r -u 1000 app
USER app
Containers run as root by default. Compromise = root in container. Always non-root.
Distroless images
FROM gcr.io/distroless/cc # for Rust / C / C++
FROM gcr.io/distroless/python3 # Python
FROM gcr.io/distroless/nodejs22 # Node
FROM gcr.io/distroless/static # static binary (Go)
Distroless images contain just enough to run; no shell, no package manager, no curl. Smaller. More secure. No surprise CVEs in unused tooling.
The tradeoff: harder to debug live containers. For dev, use a :debug variant; for prod, the bare distroless.
.dockerignore
# .dockerignore
.git
.github
node_modules
__pycache__
*.pyc
target/
dist/
.venv
.env
Keeps secrets and bloat out of the build context. Without it, COPY . pulls in your .git history.
Image scanning in CI
- uses: aquasecurity/trivy-action@master
with:
image-ref: my-app:${{ github.sha }}
severity: HIGH,CRITICAL
exit-code: 1
Fail the build on high CVEs. See Software Supply Chain Security .
Pin base image versions
FROM python:3.13.0-slim # pinned
FROM python:3.13.0-slim@sha256:abc123... # pinned + hash
FROM python:latest is a recipe for “yesterday it built fine, today it doesn’t.” Pin.
Healthcheck
HEALTHCHECK --interval=30s --timeout=3s \
CMD curl -f http://localhost:8000/healthz || exit 1
Docker / Kubernetes both honor this. Pair with Health Checks That Don’t Lie .
Common mistakes
1. COPY . . early
Invalidates cache on every code change. Copy deps first.
2. apt-get update without && apt-get install
Cached update becomes stale. Always combined.
3. Multi-stage but copying tools too
COPY --from=builder /build/target ... should copy ONE binary, not the whole target dir.
4. Running as root
Default but wrong. Always USER.
5. Latest tags
Reproducibility goes out the window. Pin.
6. Logs writing inside container
Container fs is ephemeral. Log to stdout; let the orchestrator capture.
What I’d ship today
For a typical Python service:
# syntax=docker/dockerfile:1.7
FROM python:3.13.0-slim AS builder
WORKDIR /app
RUN pip install --no-cache-dir uv
COPY pyproject.toml uv.lock ./
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --frozen --no-dev
FROM gcr.io/distroless/python3:nonroot
WORKDIR /app
COPY --from=builder --chown=nonroot:nonroot /app/.venv /app/.venv
COPY --chown=nonroot:nonroot . .
ENV PATH="/app/.venv/bin:$PATH"
HEALTHCHECK --interval=30s CMD ["python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/livez')"]
EXPOSE 8000
ENTRYPOINT ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
~150 MB. Non-root. Distroless. Cache-friendly. Builds in <30s warm.
Read this next
- Software Supply Chain Security
- Modern Python Tooling 2026
- CI/CD Best Practices in 2026
- Kubernetes for App Developers
If you want my Dockerfile templates per-language, it’s at rajpoot.dev .
Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .