Dockerfile
A text file containing ordered instructions that Docker executes to build an image layer by layer. Each instruction creates a new filesystem layer — FROM sets the base, RUN executes commands, COPY adds files, and CMD defines the startup command.
Dockerfile — The Recipe for Building Docker Images
What Is a Dockerfile in Simple Terms?
A Dockerfile is a plain text file that tells Docker how to build an image. Think of it as a recipe — it lists the ingredients (base image, files, packages) and the steps (install, configure, copy) needed to produce a working application image. Docker follows these instructions top to bottom, creating one filesystem layer per instruction, until the final image is ready.
Every Docker image starts with a Dockerfile. When you run docker build ., Docker reads the Dockerfile in the current directory and produces an image.
+------------------------------------------+| Dockerfile instructions (top to bottom) || || FROM node:20-alpine <- Layer 1 || WORKDIR /app <- Layer 2 || COPY package.json ./ <- Layer 3 || RUN npm install <- Layer 4 || COPY . . <- Layer 5 || CMD ["node", "server"] <- Layer 6 |+------------------------------------------+ | docker build | v+------------------------------------------+| Docker Image || Layer 1: node:20-alpine base || Layer 2: /app directory || Layer 3: package.json file || Layer 4: node_modules/ || Layer 5: application source code || Layer 6: default command configuration |+------------------------------------------+Every Major Dockerfile Instruction Explained
# FROM — the base image (required, must be first)FROM node:20-alpine# Always pin to a specific version — never FROM node:latest # WORKDIR — sets the working directory for all subsequent instructionsWORKDIR /app# Creates the directory if it does not exist# Equivalent to: mkdir -p /app && cd /app # COPY — copies files from the build context into the imageCOPY package.json package-lock.json ./# src (on host) -> dest (in image)# Always prefer COPY over ADD for local files # ADD — like COPY but also auto-extracts tarballs and accepts URLsADD https://example.com/config.tar.gz /etc/# Use only when you specifically need extraction or URL fetching # RUN — executes a command during the buildRUN npm ci --omit=dev# Creates a new layer with the result of the command# Chain commands to reduce layers:RUN apt-get update && \ apt-get install -y curl && \ rm -rf /var/lib/apt/lists/* # ENV — sets environment variables in the image (visible at runtime)ENV NODE_ENV=productionENV PORT=8080# Never put secrets here — visible in docker inspect and docker history # ARG — build-time variable, NOT available at runtimeARG BUILD_VERSION=unknown# Use: docker build --build-arg BUILD_VERSION=v1.0.0 .# ARG values appear in docker history — NOT safe for secrets # EXPOSE — documents which port the container listens onEXPOSE 8080# Documentation only — does NOT publish the port# Still need -p 8080:8080 in docker run # USER — switch to a non-root userUSER node# Always set this before CMD/ENTRYPOINT# All subsequent RUN, CMD, ENTRYPOINT run as this user # HEALTHCHECK — how Docker checks if the container is healthyHEALTHCHECK --interval=30s --timeout=5s --retries=3 \ CMD curl -f http://localhost:8080/health || exit 1 # ENTRYPOINT — the fixed command that always runsENTRYPOINT ["node"]# Cannot be overridden with docker run args (only with --entrypoint flag) # CMD — default arguments or commandCMD ["dist/server.js"]# With ENTRYPOINT above: runs node dist/server.js# Without ENTRYPOINT: runs node dist/server.js directly# Can be overridden: docker run myimage dist/other.js # LABEL — metadata key-value pairsLABEL maintainer="platform@swiggy.com"LABEL version="v3.1.0"Layer Caching — The Most Critical Concept
Each instruction creates a cached layer. Docker reuses cached layers if the instruction and its inputs have not changed. If any layer changes, all layers below it are invalidated.
Cache invalidation cascade: INSTRUCTION CACHE STATUS REASON----------- ------------ ------FROM node:20-alpine CACHE HIT Base image unchangedWORKDIR /app CACHE HIT UnchangedCOPY package.json . CACHE HIT package.json unchangedRUN npm install CACHE HIT Same package.json = same outputCOPY . . CACHE MISS Source code changed!RUN npm run build REBUILDING Cache miss propagates downward Result: npm install (the slow step) is CACHEDTotal build time: 20 seconds instead of 3 minutes If COPY . . came BEFORE npm install: Source change -> npm install cache bust -> 3 minute build every timeShell Form vs Exec Form
CMD, RUN, and ENTRYPOINT each support two forms:
# Shell form — runs through /bin/sh -cCMD node server.js# Actual execution: /bin/sh -c "node server.js"# Problem: node is NOT PID 1 — sh is PID 1# SIGTERM goes to sh, not to node — graceful shutdown breaks # Exec form — runs the binary directly (ALWAYS use this for CMD/ENTRYPOINT)CMD ["node", "server.js"]# Actual execution: node server.js (directly)# node IS PID 1 — SIGTERM goes directly to node# Graceful shutdown works correctlyThe .dockerignore File — Always Required
# .dockerignore — in the same directory as Dockerfilenode_modules/ <- 1GB+ — never copy from host, install inside container.git/ <- hundreds of MB — version history not needed*.log <- log files should not be in images.env <- NEVER copy secrets into imagesdist/ <- built output — rebuild inside Docker insteadcoverage/ <- test coverage reports.DS_Store <- macOS metadatadocker-compose.yml <- Compose config not needed in imageA Production-Ready Complete Dockerfile
# syntax=docker/dockerfile:1FROM node:20-alpine AS depsWORKDIR /appCOPY package.json package-lock.json ./RUN --mount=type=cache,target=/root/.npm npm ci FROM node:20-alpine AS builderWORKDIR /appCOPY --from=deps /app/node_modules ./node_modulesCOPY . .RUN npm run build FROM node:20-alpine AS productionWORKDIR /appENV NODE_ENV=production PORT=8080COPY package.json package-lock.json ./RUN npm ci --omit=dev && npm cache clean --forceCOPY --from=builder /app/dist ./distRUN addgroup -S appgroup && adduser -S appuser -G appgroup && \ chown -R appuser:appgroup /appUSER appuserEXPOSE 8080HEALTHCHECK --interval=30s --timeout=5s \ CMD wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1CMD ["node", "dist/server.js"]Troubleshooting Reference
| Problem | Cause | Fix |
|---|---|---|
| Build slow every time | COPY . . before RUN npm install | Move dependency install before COPY source |
| Container exits immediately | CMD command not found | Check binary path with docker run --rm myimage which node |
| SIGTERM not handled | Shell form CMD (CMD node server.js) |
Use exec form: CMD ["node", "server.js"] |
| Image too large | No .dockerignore, includes node_modules | Add .dockerignore, use multi-stage builds |
| Secrets visible in history | Used ENV or ARG for secrets | Use BuildKit secret mounts instead |
TipUse
docker build --progress=plain .to see the verbose output with cache hit/miss status for each layer. This is the fastest way to understand why a particular build is slower than expected.
RememberThe order of instructions in a Dockerfile directly controls build speed. Instructions that change rarely (installing system packages, installing production dependencies) go first. Instructions that change often (copying source code) go last. This maximises cache reuse.
Common MistakeCleaning up package manager caches in a separate RUN instruction.
RUN apt-get install curlfollowed byRUN rm -rf /var/lib/apt/lists/*does not reduce image size — the cache is baked into the first layer. Always clean in the same RUN:RUN apt-get install curl && rm -rf /var/lib/apt/lists/*.
SecurityNever put secrets (API keys, passwords, certificates) into a Dockerfile using ENV or ARG. Both are visible in
docker historyeven if you later delete them with another instruction. Use BuildKit--mount=type=secretfor build-time secrets and environment variables or a secret manager for runtime secrets.
Frequently Asked Questions
Why does instruction order in a Dockerfile actually matter for build speed?
Docker caches each layer and reuses it on subsequent builds as long as the instruction and its inputs haven't changed — but the moment one layer's cache is invalidated, every layer after it must rebuild too, even if nothing about them changed. That's why the standard pattern is copying dependency manifests (`package.json`, `requirements.txt`) and running the install step before copying the rest of the application source: source code changes far more often than dependencies, so this ordering keeps the expensive install layer cached across most builds.
What's the most common Dockerfile mistake that bloats final image size unnecessarily?
Using a single-stage build that includes compilers, build tools, and package caches in the final image when only the compiled output is actually needed at runtime — a Go binary or a compiled frontend bundle doesn't need the full SDK sitting in the production image. Multi-stage builds (`FROM golang AS builder`, then a second minimal `FROM` stage that just `COPY --from=builder` the artifact) fix this by discarding everything except what's explicitly copied into the final stage, often cutting image size by 10x or more for compiled languages.