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 | 指定基础镜像,alpine 体积约为完整版 1/5 | FROM node:18-alpine |
WORKDIR /app | 设置工作目录,不存在会自动创建 | WORKDIR /app |
CMD ["node","app.js"] | 默认启动命令,exec 形式可正确接收 SIGTERM | CMD ["node", "app.js"] |
ENTRYPOINT ["node"] | 固定入口点,不被 run 参数覆盖 | ENTRYPOINT ["./docker-entrypoint.sh"] |
ENV | 设置环境变量,构建期与运行期都可用 | ENV NODE_ENV=production |
ARG | 仅构建期可用的参数 | ARG VERSION=1.0 |
COPY --chown | 复制时指定属主,避免产物为 root | COPY --chown=node:node . . |
ADD vs COPY | ADD 可下载 URL 与自动解压,普通文件用 COPY | ADD file.tar.gz /app/ |
多阶段 AS builder | 命名构建阶段供后续引用 | FROM node:18 AS builder |
COPY --from | 从构建阶段或外部镜像复制产物 | COPY --from=builder /app/dist ./dist |
USER | 切换运行用户降低容器权限 | USER appuser |
HEALTHCHECK | 健康检查,连续失败标记 unhealthy | HEALTHCHECK --interval=30s CMD curl -f http://localhost/ || exit 1 |
.dockerignore | 排除 node_modules、.git 等不需要进镜像的文件 | .dockerignore |
--mount=type=cache | BuildKit 缓存挂载,跨构建复用包缓存 | RUN --mount=type=cache,target=/root/.npm npm ci |
Common pitfalls
Symptom代码改一行就要重装全部依赖,构建非常慢。
CauseCOPY . . 放在安装依赖之前,代码变更击穿了安装层的缓存。
Fix先 COPY package*.json ./ 再 RUN npm ci,装完再 COPY 剩余代码。
Symptom容器 stop 时要等十秒才退出。
Cause用了 shell 形式 CMD,shell 作为 PID 1 不转发信号给子进程。
Fix改用 exec 形式 CMD ["node", "app.js"],让应用直接作为 PID 1 接收 SIGTERM。
Symptomdocker run 传入的参数覆盖了镜像里指定的启动程序。
Cause用了 CMD 而非 ENTRYPOINT,run 后面的参数会整体替换 CMD。
FixENTRYPOINT 定住程序、CMD 只给默认参数:ENTRYPOINT ["node"] 加 CMD ["app.js"]。
Symptom镜像里能看到密码明文,删了也还在。
Cause--build-arg 传的密钥会留在构建历史中,即使后续层删掉也能翻出来。
Fix改用 BuildKit secrets(RUN --mount=type=secret);已推送的镜像需轮换密钥。
Symptom镜像体积高达数 GB,远超预期。
Cause单阶段构建把编译工具链和源码都留在最终镜像里。
Fix改多阶段构建:builder 阶段装依赖编译,最终阶段只 COPY 产物。
Symptom容器内文件属主是 root,应用进程读写自己的目录报权限错。
CauseCOPY 默认以 root 属主写入镜像。
FixCOPY --chown=node:node . .,并配合 USER 切换到非 root 用户运行。
Symptom把 node_modules 打进了镜像,本机平台的二进制在容器里跑不起来。
Cause构建上下文没排除 .dockerignore 里的 node_modules。
Fix在 .dockerignore 中排除 node_modules,让镜像内用 npm ci 安装对应平台的版本。
Symptom容器启动即退出,日志里看不出原因。
Cause启动命令路径错误或文件不可执行,Dockerfile 本身不报错。
Fix用 docker run --rm -it <image> sh 进入容器手动执行;检查 WORKDIR 与 CMD 路径是否匹配。
Troubleshooting
1进容器手动排查启动失败
docker run --rm -it <image> sh镜像不可交互时可加 --entrypoint sh;手动跑一次启动命令能看到真实报错。
2优化构建缓存命中
COPY package*.json ./依赖清单单独成层,装完依赖后再 COPY 代码,代码改动不再触发重装。
3多阶段构建只取产物
COPY --from=builder /app/dist ./dist最终镜像不含源码与工具链,体积通常能降一个数量级。
4BuildKit 缓存挂载加速安装
RUN --mount=type=cache,target=/root/.npm npm cinpm 包缓存跨构建复用,比每层重新下载快很多;需开启 BuildKit。
5确认最终镜像包含什么
docker history <image>逐层看大小来源,能直接定位是哪个 RUN / COPY 让体积暴涨。
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 再 npm ci,改业务代码只让源码层失效、依赖层继续命中缓存。这一层顺序直接决定构建耗时。
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
dist没有 .dockerignore 时整个 node_modules 会被传进构建上下文,上下文动辄几百 MB,拖慢每次构建;顺带还有把 .env 打进镜像的风险。
Scope difference between ARG and ENV
ARG VERSION=1.0 # 仅构建时可用,镜像里不存在
ENV NODE_ENV=production # 构建时与运行时都可用
RUN echo $VERSION $NODE_ENVARG 定义的变量不会自动进入后续 RUN 的环境,需要 ENV 才能在镜像里保留。切换构建阶段版本号用 ARG,运行时需要读的配置用 ENV。
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