Dockerfile Cheatsheet - Docker Image Build Reference
A Dockerfile authority for developers who want their images small, fast to finalize, and without leaked secrets. Beyond listing instructions, it explains why layer ordering and build cache matter, how multi-stage builds keep runtime images lean, and how ADD vs COPY, ENV vs ARG, USER and HEALTHCHECK change security and behavior. By the end you can write a Dockerfile that builds quickly and produces a minimal, production-safe image.
Typical Use Case
For developers who write Dockerfiles and care about image size. There are only two techniques that matter much, and both are powerful: multi-stage builds (install dependencies and toolchain at build time, ship only artefacts at run time, which can take an image from gigabytes to megabytes) and layer caching (copy the dependency manifest first so code edits do not bust the install layer). This sheet is organised into seven groups with complete examples you can adapt directly.
Base Instructions 7
FROM node:18-alpineFROM scratchWORKDIR /appCOPY package*.json ./RUN npm ciEXPOSE 3000CMD ["node", "app.js"]Env & Args 7
ENV NODE_ENV=productionENV PATH="/app/bin:$PATH"ARG VERSION=1.0ARG NPM_TOKENLABEL maintainer="email@example.com"USER appuserHEALTHCHECK --interval=30s CMD curl -f http://localhost/ || exit 1File Operations 5
COPY src/ ./src/COPY --chown=node:node . .COPY --chmod=755 entrypoint.sh /entrypoint.shADD https://example.com/file.tar.gz /tmp/ADD file.tar.gz /app/Multi-stage Build 5
FROM node:18 AS builderCOPY --from=builder /app/dist ./distCOPY --from=nginx:alpine /etc/nginx/nginx.conf /etc/nginx/FROM golang:1.21 AS builderFROM alpine:3.18Entrypoint 5
CMD ["node", "app.js"]ENTRYPOINT ["node"]ENTRYPOINT ["node"]
CMD ["app.js"]SHELL ["/bin/bash", "-c"]ENTRYPOINT ["./docker-entrypoint.sh"]Build Optimization 6
COPY package*.json ./RUN npm ci && npm cache clean --forceRUN apk add --no-cache curl.dockerignoreRUN --mount=type=cache,target=/root/.npm npm ciFROM node:18-alpineFull Examples 3
FROM node:18 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:18-alpine
WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
CMD ["node", "dist/index.js"]FROM python:3.11 AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --user -r requirements.txt
FROM python:3.11-slim
WORKDIR /app
COPY --from=builder /root/.local /root/.local
COPY . .
CMD ["python", "main.py"]FROM golang:1.21 AS builder
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /app/server .
FROM alpine:3.18
COPY --from=builder /app/server /app/server
CMD ["/app/server"]Parameter matrix
| Parameter | Effect | Example |
|---|---|---|
FROM node:18-alpine | Specifies the base image; alpine is about 1/5 the size of the full variant. | FROM node:18-alpine |
WORKDIR /app | Sets the working directory, creating it automatically if missing. | WORKDIR /app |
CMD ["node","app.js"] | Default startup command; the exec form receives SIGTERM correctly. | CMD ["node", "app.js"] |
ENTRYPOINT ["node"] | Fixes the entry point so run arguments cannot override it. | ENTRYPOINT ["./docker-entrypoint.sh"] |
ENV | Sets environment variables, available both at build time and runtime. | ENV NODE_ENV=production |
ARG | A parameter available only at build time. | ARG VERSION=1.0 |
COPY --chown | Sets the owner when copying, avoiding root-owned artifacts. | COPY --chown=node:node . . |
ADD vs COPY | ADD can download URLs and auto-extract; use COPY for ordinary files. | ADD file.tar.gz /app/ |
multi-stage AS builder | Names a build stage for later reference. | FROM node:18 AS builder |
COPY --from | Copies artifacts from a build stage or an external image. | COPY --from=builder /app/dist ./dist |
USER | Switches the runtime user to lower container privileges. | USER appuser |
HEALTHCHECK | Health check; consecutive failures mark the container unhealthy. | HEALTHCHECK --interval=30s CMD curl -f http://localhost/ || exit 1 |
.dockerignore | Excludes files such as node_modules and .git that need not enter the image. | .dockerignore |
--mount=type=cache | BuildKit cache mount, reusing package caches across builds. | RUN --mount=type=cache,target=/root/.npm npm ci |
Common pitfalls
SymptomChanging a single line of code reinstalls all dependencies and the build is very slow.
CauseCOPY . . comes before installing dependencies, so code changes bust the cache of the install layer.
FixCOPY package*.json ./ first, then RUN npm ci, and only after that COPY the remaining code.
SymptomThe container takes ten seconds to stop.
CauseIt uses the shell form of CMD, and the shell as PID 1 does not forward signals to child processes.
FixSwitch to the exec form CMD ["node", "app.js"] so the app receives SIGTERM directly as PID 1.
SymptomArguments passed to docker run override the startup program specified in the image.
CauseCMD was used instead of ENTRYPOINT, so arguments after run replace CMD entirely.
FixPin the program with ENTRYPOINT and give defaults via CMD only: ENTRYPOINT ["node"] plus CMD ["app.js"].
SymptomA password is visible in plaintext in the image and stays even after deletion.
CauseSecrets passed via --build-arg remain in the build history and can be dug out even if a later layer deletes them.
FixUse BuildKit secrets (RUN --mount=type=secret); images already pushed need their secrets rotated.
SymptomThe image is several GB, far larger than expected.
CauseA single-stage build leaves the compile toolchain and source in the final image.
FixSwitch to a multi-stage build: a builder stage installs deps and compiles, and the final stage only COPYs the artifacts.
SymptomFiles in the container are owned by root, and the app process hits permission errors reading/writing its own directory.
CauseCOPY writes files into the image owned by root by default.
FixUse COPY --chown=node:node . . and pair it with USER to run as a non-root user.
Symptomnode_modules was bundled into the image and host-platform binaries do not run in the container.
CauseThe build context did not exclude node_modules via .dockerignore.
FixExclude node_modules in .dockerignore so the image installs the matching platform build with npm ci.
SymptomThe container exits immediately on start and the logs give no clue.
CauseThe startup command path is wrong or the file is not executable, and the Dockerfile itself reports no error.
FixEnter the container with docker run --rm -it <image> sh and run it by hand; check that WORKDIR and the CMD path match.
Troubleshooting
1Enter the container to debug a failed startup by hand.
docker run --rm -it <image> shIf the image is not interactive, add --entrypoint sh; running the startup command once by hand reveals the real error.
2Improve build cache hits.
COPY package*.json ./Keep the dependency manifest in its own layer and COPY code only after dependencies are installed, so code changes no longer trigger reinstallation.
3Multi-stage build taking only artifacts.
COPY --from=builder /app/dist ./distThe final image contains no source or toolchain; size typically drops by an order of magnitude.
4BuildKit cache mount to speed up installs.
RUN --mount=type=cache,target=/root/.npm npm ciThe npm package cache is reused across builds, far faster than re-downloading per layer; requires BuildKit enabled.
5Confirm what the final image contains.
docker history <image>Look at size by layer to pinpoint exactly which RUN / COPY ballooned the image.
Command Examples
Node multi-stage build: full toolchain at build time, artifacts only at runtime
FROM node:18 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:18-alpine
WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
CMD ["node", "dist/index.js"]COPY the dependency files first, then npm ci, so editing business code only invalidates the source layer while the dependency layer keeps hitting cache. This layer ordering directly determines build time.
Output
STEP 5/12 : RUN npm run build ---> Running in 8f3a1c > app@1.0.0 build > vite build vite v5.0.0 building for production... ✓ 1423 modules transformed. dist/index.html 0.46 kB Successfully built in 12.34s
Keep the build context small with .dockerignore
# .dockerignore
node_modules
.git
*.log
.env
distWithout .dockerignore the whole node_modules is sent into the build context, which runs to hundreds of MB and slows every build — plus there is a risk of baking .env into the image.
Scope difference between ARG and ENV
ARG VERSION=1.0 # build-time only, absent from the image
ENV NODE_ENV=production # available at both build and run time
RUN echo $VERSION $NODE_ENVA variable defined by ARG does not automatically enter subsequent RUN environments; ENV is needed to keep it in the image. Use ARG for a build-stage version number, ENV for config the runtime reads.
Output
1.0 production
Common Pitfalls
- Secrets baked into ARG or ENV remain in the image layer permanently — deleting them in a later layer does not hide them from docker history.
- Bad COPY ordering invalidates the dependency cache, turning a seconds-long build into minutes on every code change.
- A single-stage build keeps the entire toolchain and source in the final image, easily adding hundreds of MB; use multi-stage and copy only artifacts.
- Running as root means a container escape immediately yields root; create a non-root user and switch with a USER instruction.
- CMD is replaced wholesale by arguments after docker run, which loses the program path; fix the program with ENTRYPOINT.
Tips
- Each RUN/COPY/ADD instruction creates a new layer — merge RUN commands to reduce layer count.
- Multi-stage builds greatly shrink the final image, ideal for compiled languages like Go, Rust, Java.
- Use .dockerignore to exclude node_modules, .git, .env and other files not needed for the build.
- CMD vs ENTRYPOINT: CMD can be overridden by docker run arguments; ENTRYPOINT is fixed.
- Prefer the exec form ["cmd","arg"] over the shell form — it receives SIGTERM correctly and avoids stop timeouts.
- Don't pass secrets via --build-arg; they persist in the image build history. Use BuildKit secrets instead.
FAQ
Why does the order of COPY instructions in a Dockerfile matter?
Every RUN, COPY or ADD produces a cached layer, and any change invalidates everything below it. Copying package*.json first and running npm ci afterwards keeps the dependency layer cached, so editing application code only invalidates the source layer. In the reverse order, every code change forces a full dependency reinstall and turns a few-second build into minutes.
What problem does multi-stage building solve?
It separates build-time from runtime dependencies: the builder stage installs the full toolchain and compiles, while the final stage FROMs a slim base and copies in artifacts with COPY --from=builder. Compilers, headers and dev dependencies never reach the final image. The effect is most dramatic for Node projects, where the runtime image ends up holding only dist and node_modules.
What is the difference between ARG and ENV?
ARG is a build-time parameter passed with --build-arg and it does not exist once the build finishes. ENV is both a build-time and a runtime environment variable and is baked into the image. Use ARG for one-off values like a stage version tag, and ENV for anything the running container must see, such as NODE_ENV. A common gotcha: an ARG value is not automatically available to later RUN steps unless you also declare it as ENV.
How should CMD and ENTRYPOINT be combined?
ENTRYPOINT fixes the executable and is not easily replaced by arguments after docker run, while CMD supplies the default arguments and is substituted by them. The usual pattern is ENTRYPOINT ["node"] with CMD ["app.js"], so the default run is node app.js while docker run image other.js becomes node other.js. If the whole command lives in CMD, the program itself is lost when it is overridden.
Should I use COPY or ADD?
Use COPY for ordinary file copying — its behavior is explicit. ADD additionally fetches a URL and auto-extracts local tarballs. Reserve it for those two cases, such as ADD package.tar.gz /app/ when you want extraction; otherwise prefer COPY to avoid surprises from implicit unpacking. Remember to pair the build with a .dockerignore excluding node_modules, .git and similar.
Official References
Each command links to its official documentation below, so you can verify the latest usage and read deeper.
Maintained by LaoHand
Publicly updated on Jul 21, 2026, continuously proofread against official docs.
Contact Us
Wrong command or description? Send us corrections, business inquiries or product feedback by email.
Contact Us