A Dockerfile is a container’s "recipe". Writing good Dockerfiles is fundamental to using containers efficiently.
Dockerfile basics#
Your first Dockerfile
# 基于什么镜像开始
FROM python:3.12-slim
# 设置工作目录(后续命令都在这个目录执行)
WORKDIR /app
# 复制依赖文件(先复制 requirements 是为了利用层缓存)
COPY requirements.txt .
# 安装依赖
RUN pip install --no-cache-dir -r requirements.txt
# 复制应用代码
COPY . .
# 声明容器运行时监听的端口(文档性质,不影响运行)
EXPOSE 8000
# 容器启动时执行的命令
CMD ["python", "app.py"]Building and running
# 构建镜像
docker build -t myapp:1.0 .
# └─┘ └─────┘ └─┘
# 打标签 名称:版本 构建上下文(此例为当前目录;Dockerfile 可由 -f 另选)
# 运行
docker run -d -p 8000:8000 myapp:1.0Core instruction quick reference#
| Instruction | Purpose | Example |
|---|---|---|
FROM |
Specifies the base image for a stage; global ARG instructions and parser directives may appear before the first FROM | FROM node:20-alpine |
WORKDIR |
Sets the working directory, creating it automatically if it does not exist | WORKDIR /app |
COPY |
Copies from the build context or a specified stage/image/context | COPY . /app/ |
ADD |
Similar to COPY, with additional support for URL downloads and automatic tar extraction | ADD archive.tar.gz /app/ |
RUN |
Runs commands while building the image | RUN apt update && apt install -y curl |
ENV |
Sets environment variables (available at both build time and runtime) | ENV NODE_ENV=production |
ARG |
Build arguments (available only at build time) | ARG VERSION=1.0 |
EXPOSE |
Declares a port (documentation only; actual publishing requires -p) |
EXPOSE 3000 |
CMD |
The command executed when the container starts (can be overridden by arguments at the end of docker run) |
CMD ["node", "server.js"] |
ENTRYPOINT |
The container entry point (can be overridden with --entrypoint; exec-form CMD usually provides default arguments) | ENTRYPOINT ["python"] |
USER |
Switches the execution user (security hardening) | USER appuser |
HEALTHCHECK |
Health check command | HEALTHCHECK --interval=30s CMD curl -f http://localhost/ || exit 1 |
CMD vs ENTRYPOINT
# 方式 1:只有 CMD(可被覆盖)
CMD ["python", "app.py"]
# docker run myapp node server.js ← 会覆盖 CMD
# 方式 2:ENTRYPOINT + CMD(可用 --entrypoint 覆盖 ENTRYPOINT,CMD 作为默认参数)
ENTRYPOINT ["python"]
CMD ["app.py"]
# docker run myapp --version ← 相当于 python --version
# 方式 3:ENTRYPOINT 执行脚本
ENTRYPOINT ["/docker-entrypoint.sh"]
# 脚本可以做初始化(数据库迁移、权限设置),最后 exec "$@" 启动 CMDMulti-stage builds#
The following snippets are independent examples based on each sample project’s requirements/package-lock and entry-point files. They cannot simply be concatenated into a single general-purpose image.
Goal: Use a large image containing compilation tools for the build stage, and a small image containing only the runtime for the final artifact.
Single-stage (poor)
FROM node:20
WORKDIR /app
COPY . .
RUN npm ci && npm run build
CMD ["node", "dist/index.js"]
# 最终镜像包含构建依赖和源码,具体大小以实际构建测量Multi-stage (better)
# 阶段 1:构建(build stage)
FROM node:20 AS builder
WORKDIR /app
COPY package*.json .
RUN npm ci
COPY . .
RUN npm run build
# 阶段 2:运行(production stage)
FROM node:20-slim
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY package*.json .
RUN npm ci --omit=dev
USER node
CMD ["node", "dist/index.js"]
# 最终阶段只装生产依赖和运行产物;需验证本机模块 ABI 与运行需求Effect: This can reduce build tools and unrelated files. Check the actual image size, vulnerability count, and runtime compatibility after building.
Optimizing the layer cache#
Dockerfile instructions create build steps and metadata; not every instruction creates a filesystem layer. RUN/COPY and similar instructions usually change the filesystem. Cache hits depend on the instructions, context, and dependency inputs.
Layer cache rules
FROM python:3.12-slim # 层 1
WORKDIR /app # 层 2
COPY requirements.txt . # 层 3 ← 如果 requirements.txt 没变,这层用缓存
RUN pip install -r requirements.txt # 层 4 ← 依赖不装在上层,也用缓存
COPY . . # 层 5 ← 源代码变了,这层和后面都重新构建
CMD ["python", "app.py"] # 层 6Key principle: Put infrequently changing content first and frequently changing content last.
# ✅ 好:先 COPY 依赖文件(不常变),再 COPY 代码(常变)
COPY package.json package-lock.json .
RUN npm ci
COPY . .
# ❌ 差:先 COPY 全部(轻微改动就废掉所有缓存)
COPY . .
RUN npm ciParallel builds
# 启用 BuildKit(Docker 23+ 默认, 更老版本需要设置)
export DOCKER_BUILDKIT=1
# BuildKit 可并行不相互依赖的阶段/构建图节点;同阶段有先后依赖的 RUN 仍顺序执行
docker build -t myapp .Optimization tips at a glance#
# 1. 合并 RUN 减少层数(用 && 连接)
RUN apt-get update && apt-get install -y curl vim && rm -rf /var/lib/apt/lists/*
# 2. 使用 .dockerignore 排除不需要的文件
# .dockerignore:
# node_modules
# .git
# *.md
# .env
# 3. 指定非 root 用户
RUN useradd -m appuser
USER appuser
# 4. 用 Alpine 等精简镜像(需验证 musl、原生依赖与调试工具兼容性)
FROM node:20-alpine # 大小依版本及架构
# vs FROM node:20 # 实际大小需检查
# 5. 用 slim 变体(平衡大小和兼容性)
FROM python:3.12-slim # 大小依版本及架构
# vs FROM python:3.12 # 实际大小需检查
# 6. 明确版本;示例版本是历史示意,标签仍可变,生产可进一步锁 digest
FROM node:20.11.1-alpine3.19
# 而不是 FROM node:latest(今天和明天拉的镜像可能不同)Build secrets and entry-point behavior#
ARG/ENV, COPY, and ordinary RUN instructions cannot safely hold secrets; deleting a file from an image does not guarantee that earlier layers contain no secrets. Use BuildKit secret/SSH mounts for build credentials, and check that build commands do not write them into artifacts. Use an exec-form entry point or exec "$@" at the end of an initialization script to forward stop signals correctly. The example image tags are teaching inputs, not recommendations for currently supported versions.
References & further reading
Adapted from technical notes. Examples use generic names.
Continue along a reading path
Docker: from running containers to application orchestration
- 01Docker basics: images, containers, and the daemon
- 02The Docker command line: images, containers, volumes, and networks
- 03Dockerfiles and image builds
- 04Compose: defining multi-service applications
- 05Docker networking and storage
- 06Docker in production: resources, security, and recovery
- 07Declaring Docker and containers on NixOS
- 08Swarm: from service replicas to cluster orchestration
- 09Docker and Podman: architecture, compatibility, and coexistence