Writing Dockerfiles
Learn to write Dockerfiles from scratch - every instruction explained, with patterns and pitfalls from real-world usage.
Writing Dockerfiles
A Dockerfile is the source of truth for your container image. It's a plain text file - a sequence of instructions that Docker executes top-to-bottom to produce a repeatable, portable image. Master it, and you can package any application to run anywhere.
Why Dockerfiles Matter
Before Dockerfiles, deploying software meant documenting a maze of manual steps: install this library, edit that config file, set these environment variables. Dockerfiles replace all of that with executable documentation - a script that is also the deployment artifact.
# This 8-line file replaces a 20-page deployment guide
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
EXPOSE 3000
USER node
CMD ["node", "server.js"]
Every Instruction at a Glance
| Instruction | Purpose |
|---|---|
FROM | Set the base image |
WORKDIR | Set the working directory |
COPY | Copy files from host into the image |
ADD | Like COPY, with URL and archive support |
RUN | Execute a command at build time |
ENV | Set environment variables (build + runtime) |
ARG | Set build-time-only variables |
EXPOSE | Document which ports the app uses |
USER | Switch to a non-root user |
VOLUME | Declare a mount point |
LABEL | Add metadata key-value pairs |
CMD | Default command when container starts |
ENTRYPOINT | Fixed executable for the container |
HEALTHCHECK | Define a health check command |
ONBUILD | Trigger instructions for child images |
A Complete Annotated Example
# ── Base ─────────────────────────────────────────────────────
FROM node:20-alpine
# Use a specific, small base image. Alpine is ~5MB vs ~1GB for node:latest.
# ── Metadata ─────────────────────────────────────────────────
LABEL maintainer="team@example.com"
LABEL version="1.0.0"
LABEL description="My production web API"
# ── Working Directory ─────────────────────────────────────────
WORKDIR /app
# All subsequent instructions run relative to /app.
# ── Dependencies (cached layer) ──────────────────────────────
COPY package*.json ./
RUN npm ci --omit=dev
# Copy manifest first → install → then copy source.
# This way npm ci only reruns when package.json changes.
# ── Application Code ──────────────────────────────────────────
COPY --chown=node:node . .
# Copy source last (changes most often).
# --chown ensures the app user owns the files.
# ── Runtime Config ────────────────────────────────────────────
ENV NODE_ENV=production
ENV PORT=3000
EXPOSE 3000
# ── Security ──────────────────────────────────────────────────
USER node
# Never run as root in production.
# ── Startup ───────────────────────────────────────────────────
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
CMD wget -qO- http://localhost:3000/health || exit 1
CMD ["node", "server.js"]
How the Build Works
Docker Build: Layer Caching Flow
Put instructions that change rarely near the top — they stay cached longer
Each instruction that changes the filesystem creates an immutable layer. Layers are stacked to form the final image. This is why instruction order matters so much for build speed.
What's Covered in This Section
| Subsection | What You'll Learn |
|---|---|
| FROM | Choosing base images, variants, multi-stage FROM, scratch |
| COPY & ADD | Copying files, ownership, .dockerignore, when to use ADD |
| RUN | Build-time commands, layer efficiency, shell vs exec form |
| CMD & ENTRYPOINT | Default commands, exec vs shell form, combining both |
| ENV & ARG | Runtime vs build-time variables, defaults, security |
| WORKDIR & EXPOSE | Working directories, port documentation |
| Best Practices | Security, caching, size, linting, non-root users |
| Real-World Examples | Complete Dockerfiles for Node, Python, Go, and React apps |
Your First Dockerfile
Try building a minimal image right now:
Build from a Dockerfile
ConceptThe current directory contains a simple Dockerfile. Build it and tag the image as 'myapp:v1'.
The .dockerignore File
Before Docker sends your files to the build daemon, it filters them through .dockerignore. This is always the first thing to set up:
# .dockerignore
node_modules/
.git/
.env
.env.*
*.log
coverage/
dist/
.DS_Store
.vscode/
README.md
tests/
Without it, COPY . . might copy gigabytes of node_modules or accidentally bake secrets into your image. With it, the build context shrinks to just what the Dockerfile actually needs.
Key Mental Model
Think of a Dockerfile as two distinct phases:
Build time - Everything that runs during docker build:
FROM,WORKDIR,COPY,ADD,RUN,ARG,LABEL,USER,EXPOSE,VOLUME
Runtime - Everything that activates when a container starts:
ENV,CMD,ENTRYPOINT,HEALTHCHECK
RUN is build-time. CMD is runtime. Confusing these two is the most common beginner mistake.
RUN npm run build # ✓ Runs ONCE during build, output baked into image
CMD ["npm", "start"] # ✓ Runs EVERY TIME a container starts