Skip to main content

Dockerfile — The Recipe Card

A Dockerfile is a text file with step-by-step instructions for building a Docker image. Think of it as a recipe card — each line tells the kitchen what to do, and the result is a packed lunch box (an image) that anyone can open.

Anatomy of a Dockerfile​

A real Dockerfile for Campus Library
FROM node:20-alpine # Start with a base kitchen
WORKDIR /app # Set the cooking counter
COPY package*.json ./ # Copy ingredient list first
RUN npm install # Install dependencies
COPY . . # Copy all source code
EXPOSE 3000 # Tell Docker the app uses port 3000
CMD ["node", "server.js"] # What to run when the lunch box opens

Each instruction explained​

InstructionWhat it doesLunch box meaning
FROMSets the base image"Start with this kitchen"
WORKDIRSets the working directory"Set the counter here"
COPYCopies files from your computer into the image"Bring ingredients from home"
RUNExecutes a command during build"Cook the ingredients"
EXPOSEDocuments which port the app uses"The lunch box has a straw hole"
CMDDefault command when the container starts"When opened, serve this dish"
Remember

RUN happens at build time (when you pack). CMD happens at run time (when someone opens the lunch box).

Layers — The Tupperware Stack​

Every instruction in a Dockerfile creates a layer — like stacking Tupperware containers. Docker caches layers, so if only one layer changes, the others are reused.

Two Dockerfiles — which builds faster?
# Version A — no cache
FROM node:20-alpine
COPY . .
RUN npm install
Version B — layer caching (recommended)
FROM node:20-alpine
COPY package*.json ./ # Layer 1: only changes when package.json changes
RUN npm install # Layer 2: cached unless package.json changed
COPY . . # Layer 3: only this rebuilds when code changes

Version B is much faster on rebuilds because npm install is cached until package.json changes.

Common mistake

Copying everything first (COPY . .) before RUN npm install means npm install reruns every time any file changes — even a comment in server.js. Always copy dependency files first.

Best Practices​

1. Use specific base images​

Bad — unpredictable
FROM node
Good — pinned version
FROM node:20-alpine

2. Minimize layers​

Bad — 3 layers
RUN apt-get update
RUN apt-get install -y curl
RUN rm -rf /var/lib/apt/lists/*
Good — 1 layer
RUN apt-get update && \
apt-get install -y curl && \
rm -rf /var/lib/apt/lists/*

3. Use .dockerignore​

Just like .gitignore keeps scraps out of Git, .dockerignore keeps unnecessary files out of your image:

.dockerignore
node_modules
.git
.env
*.log

4. Don't run as root​

Run as non-root user
FROM node:20-alpine
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
WORKDIR /app
COPY --chown=appuser:appgroup . .
USER appuser
CMD ["node", "server.js"]
Remember

Running as root inside a container is a security risk. Always create a non-root user.

Multi-Stage Builds — Two Kitchens, One Lunch Box​

Sometimes you need a big kitchen to cook (build tools, compilers) but a small kitchen to serve (runtime only). Multi-stage builds use multiple FROM instructions:

Multi-stage build — build in Node, serve with Nginx
# Stage 1: Build
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build

# Stage 2: Serve
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

The final image only contains Nginx and the built files — no Node.js, no node_modules, no source code. Result: 187MB instead of 1.2GB.

Remember

Multi-stage builds are the "cook in a big kitchen, serve in a small box" trick. They dramatically reduce image size and attack surface.

.env Files — Secret Ingredients​

Never hardcode secrets in a Dockerfile. Use environment variables:

Dockerfile — using ARG and ENV
ARG APP_VERSION=1.0.0
ENV NODE_ENV=production
Pass secrets at runtime, not build time
docker run -e DB_PASSWORD=secret campus-library

Or use a .env file with docker-compose:

docker-compose.yml
services:
web:
build: .
env_file:
- .env
Common mistake

Hardcoding passwords in a Dockerfile means they're baked into the image layers — visible to anyone who runs docker history. Always use environment variables or secrets management.

Build Arguments vs Environment Variables​

FeatureARGENV
Available at build timeYesYes
Available at run timeNoYes
Visible in final imageNoYes
Use caseVersion numbers, build flagsRuntime config, secrets
ARG for build, ENV for runtime
ARG BUILD_DATE
ENV APP_PORT=3000
Remember

ARG is for the kitchen during cooking. ENV is for the lunch box after it's packed.