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
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
| Instruction | What it does | Lunch box meaning |
|---|---|---|
FROM | Sets the base image | "Start with this kitchen" |
WORKDIR | Sets the working directory | "Set the counter here" |
COPY | Copies files from your computer into the image | "Bring ingredients from home" |
RUN | Executes a command during build | "Cook the ingredients" |
EXPOSE | Documents which port the app uses | "The lunch box has a straw hole" |
CMD | Default command when the container starts | "When opened, serve this dish" |
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.
# Version A — no cache
FROM node:20-alpine
COPY . .
RUN npm install
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.
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
FROM node
FROM node:20-alpine
2. Minimize layers
RUN apt-get update
RUN apt-get install -y curl
RUN rm -rf /var/lib/apt/lists/*
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:
node_modules
.git
.env
*.log
4. Don't run as root
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"]
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:
# 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.
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:
ARG APP_VERSION=1.0.0
ENV NODE_ENV=production
docker run -e DB_PASSWORD=secret campus-library
Or use a .env file with docker-compose:
services:
web:
build: .
env_file:
- .env
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
| Feature | ARG | ENV |
|---|---|---|
| Available at build time | Yes | Yes |
| Available at run time | No | Yes |
| Visible in final image | No | Yes |
| Use case | Version numbers, build flags | Runtime config, secrets |
ARG BUILD_DATE
ENV APP_PORT=3000
ARG is for the kitchen during cooking. ENV is for the lunch box after it's packed.