Dockerfile Best Practices: From Basics to Production‑Grade Images

Most Docker problems don’t start at runtime.
They start in the Dockerfile.

A weak Dockerfile leads to:

  • huge images
  • slow CI pipelines
  • security vulnerabilities
  • containers that behave differently in production

A good Dockerfile makes Docker boring — predictable, fast, and secure.

This post walks from basic → intermediate → expert Dockerfile practices, using real examples and reasoning, not rules without context.


Part 1: Dockerfile Basics (Write What Docker Expects)

1. Start With a Clear, Minimal Base Image

Bad:

FROM ubuntu:latest

Better:

FROM python:3.11-slim

Why this matters:

  • smaller base images reduce size and attack surface
  • latest is unpredictable and can break builds

Docker recommends choosing trusted, minimal base images and pinning versions


2. Use WORKDIR (Don’t Rely on cd)

Bad:

RUN cd /app && python app.py

Good:

WORKDIR /app
COPY . .

Each Dockerfile instruction runs in its own layer.
WORKDIR makes intent explicit and reliable.


3. COPY Is Better Than ADD (Most of the Time)

Use:

COPY requirements.txt .

Avoid ADD unless you need:

  • auto‑extract archives
  • remote URLs

Keeping instructions explicit makes images easier to reason about.


Part 2: Intermediate Practices (Speed and Size Matter)

4. Order Instructions for Build Cache Efficiency

Docker caches layers.
If a layer changes, all layers after it rebuild.

Bad order:

COPY . .
RUN pip install -r requirements.txt

Good order:

COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .

This simple change can cut CI build time drastically by reusing cached dependency layers.


5. Use .dockerignore (Often Forgotten, Very Important)

Without .dockerignore, Docker sends everything in your directory as build context.

Create .dockerignore:

.git
node_modules
__pycache__
.env
tests/

This reduces:

  • build time
  • image size
  • accidental secret leaks

6. Prefer Slim or Alpine — But Know the Trade‑Off

Options:

  • *-slim → safe default for production
  • *-alpine → very small, but uses musl libc
  • distroless → minimal runtime, no shell

Docker docs recommend using different images for build vs runtime when needed.


Part 3: Expert Practices (Production‑Grade Dockerfiles)

7. Multi‑Stage Builds (The Biggest Upgrade)

Multi‑stage builds separate:

  • build tools
  • runtime environment

Example (Node.js):

# Build stage
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json .
RUN npm ci
COPY . .
RUN npm run build

# Runtime stage
FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
CMD ["node", "dist/index.js"]

Benefits:

  • smaller final image
  • no build tools in production
  • faster pulls and startups

Docker explicitly recommends multi‑stage builds for production images.


8. Don’t Run Containers as Root

By default, containers run as root.

Create a non‑root user:

RUN adduser -D appuser
USER appuser

Why this matters:

  • limits blast radius if container is compromised
  • required by platforms like OpenShift

Security best‑practice sources consistently highlight this as critical.


9. Never Store Secrets in Dockerfiles

Bad:

ENV DB_PASSWORD=secret123

Why this is dangerous:

  • Docker layers are inspectable
  • secrets become part of image history

Correct approach:

  • pass secrets at runtime
  • use secret managers

Docker security guidance strongly warns against baking secrets into images.


10. Clean Package Manager Caches

Bad:

RUN apt-get update && apt-get install -y curl

Good:

RUN apt-get update && apt-get install -y curl \
    && rm -rf /var/lib/apt/lists/*

This prevents unnecessary files from being permanently stored in image layers, reducing size and exposure.

A Production‑Ready Example Dockerfile

FROM python:3.11-slim
WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

RUN adduser --disabled-password appuser
USER appuser

CMD ["python", "main.py"]

This Dockerfile is: ✅ small
✅ cache‑friendly
✅ non‑root
✅ predictable


How to Think About Dockerfiles (Mental Model)

Think of a Dockerfile as:

a contract between build time and runtime

Anything not required at runtime should not exist in the final image.

If you design with that mindset:

  • images get smaller
  • security improves naturally
  • builds become faster

InfraDecode Takeaway

A Dockerfile is not just a build script.
It’s an operational design decision.

Teams that master Dockerfiles rarely fight Docker in production.

— InfraDecode


Discover more from

Subscribe to get the latest posts sent to your email.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top