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

InstructionPurpose
FROMSet the base image
WORKDIRSet the working directory
COPYCopy files from host into the image
ADDLike COPY, with URL and archive support
RUNExecute a command at build time
ENVSet environment variables (build + runtime)
ARGSet build-time-only variables
EXPOSEDocument which ports the app uses
USERSwitch to a non-root user
VOLUMEDeclare a mount point
LABELAdd metadata key-value pairs
CMDDefault command when container starts
ENTRYPOINTFixed executable for the container
HEALTHCHECKDefine a health check command
ONBUILDTrigger 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

Dockerfile instruction
docker build reads instruction
Already cached?
Yes
Reuse layer
(fast)
No
Execute instruction
Create new layer
Cache the layer

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

SubsectionWhat You'll Learn
FROMChoosing base images, variants, multi-stage FROM, scratch
COPY & ADDCopying files, ownership, .dockerignore, when to use ADD
RUNBuild-time commands, layer efficiency, shell vs exec form
CMD & ENTRYPOINTDefault commands, exec vs shell form, combining both
ENV & ARGRuntime vs build-time variables, defaults, security
WORKDIR & EXPOSEWorking directories, port documentation
Best PracticesSecurity, caching, size, linting, non-root users
Real-World ExamplesComplete Dockerfiles for Node, Python, Go, and React apps

Your First Dockerfile

Try building a minimal image right now:

Build from a Dockerfile

Concept

The current directory contains a simple Dockerfile. Build it and tag the image as 'myapp:v1'.

▸ your answerPress Run to check
answer

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

Continue Learning

Explore Related Topics

Try the Tool

Related Resources