Skip to content

Dockerfile 写法

Dockerfile 是镜像构建的说明书。写得清楚,镜像就能复现;写得随意,后面发布、回滚、扫描和排查都会变成猜。写 Dockerfile 时先保证可复现,再考虑瘦身——可复现是发布和回滚的底线。

基本结构和指令

一个常见的 Python 应用 Dockerfile:

dockerfile
# 固定基础镜像版本,避免 latest 指向变化导致构建结果漂移
FROM python:3.12-slim

# 设置工作目录,后续 COPY/RUN/CMD 都以此为基准
WORKDIR /app

# 先复制依赖文件,利用构建缓存——依赖变化少,代码变化多
COPY requirements.txt .

# 安装依赖后清理 pip 缓存,减少镜像层体积
RUN pip install --no-cache-dir -r requirements.txt

# 再复制应用代码,代码变更时只影响从这一行开始的层
COPY . .

# 声明容器内服务端口,真正暴露还要靠 docker run -p 或 Compose ports
EXPOSE 8000

# 使用 exec 形式,进程作为 PID 1 能正确收到 SIGTERM
CMD ["python", "app.py"]

常用指令:

指令作用
FROM指定基础镜像,必须是第一条指令
WORKDIR设置工作目录,后续指令的相对路径以此为基准
COPY从构建上下文复制文件到镜像
RUN构建时执行命令,每个 RUN 生成一层
ENV设置环境变量,会留在镜像层和容器运行时
EXPOSE声明端口,纯文档性质——真正暴露靠运行时 -p
USER指定运行用户
ENTRYPOINT固定入口程序
CMD默认启动参数或命令

Dockerfile 不是 shell 脚本。每个 RUN 生成一层,文件复制和删除要考虑中间产物有没有留在最终镜像里。

COPY 和 ADD 的区别

dockerfile
COPY app.py /app/app.py
COPY config/ /app/config/

ADD 额外支持自动解压 tar 和从 URL 添加文件。语义上 COPY 更直接,只有确认需要自动解压本地 tar 包时才用 ADD。日常构建优先 COPY

RUN 合并与构建缓存

安装系统包时把更新索引、安装、清理缓存放在同一个 RUN 里:

dockerfile
RUN apt-get update \
    && apt-get install -y --no-install-recommends curl ca-certificates \
    && rm -rf /var/lib/apt/lists/*

拆成多个 RUN 的话,即使后面删除了缓存,前面的层里仍然可能保留缓存文件——镜像体积就白白大了。

构建缓存跟指令顺序强相关。Docker 逐条执行指令,如果某条指令的结果在缓存中(前一条指令相同、本条指令内容相同),就直接复用缓存。所以通常先 COPY 依赖声明(变化少),再安装依赖,最后 COPY 应用代码(变化多)——这样代码变更不会让前面的依赖安装层缓存失效。

CMD 和 ENTRYPOINT

CMD 提供默认命令,可以被 docker run 后面的命令覆盖:

dockerfile
CMD ["nginx", "-g", "daemon off;"]

ENTRYPOINT 做固定入口,CMD 做默认参数:

dockerfile
ENTRYPOINT ["python", "worker.py"]
CMD ["--log-level=info"]

运行时:

bash
docker run --rm demo-worker --log-level=debug  # 只覆盖 CMD 参数,ENTRYPOINT 不变

服务类镜像的主进程必须前台运行。把进程丢到后台后,容器的 PID 1 退出,容器也跟着退出。这是容器和虚拟机的一个关键差异——容器没有 init 系统去接管孤儿进程。

非 root 用户

很多官方镜像默认 root 运行。业务服务能不用 root 就不用:

dockerfile
FROM python:3.12-slim

WORKDIR /app

# 创建固定 UID/GID,方便和宿主机挂载目录对齐权限
RUN groupadd -g 10001 app \
    && useradd -u 10001 -g app -s /usr/sbin/nologin app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY --chown=app:app . .

USER app
CMD ["python", "app.py"]

容器内 root 不等于宿主机 root,但风险仍然更高。挂载宿主机目录、开启特权容器、Docker socket 暴露出来时,root 的风险会被放大。

多阶段构建

多阶段构建把编译和运行环境拆成两个阶段。前一阶段编译,后一阶段只复制运行需要的文件,编译工具、源码缓存、测试文件不会进入最终镜像:

dockerfile
# 第一阶段:编译
FROM golang:1.22 AS builder
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o /out/app ./cmd/app

# 第二阶段:只保留运行文件
FROM alpine:3.20
RUN adduser -D -u 10001 app
COPY --from=builder /out/app /usr/local/bin/app
USER app
ENTRYPOINT ["/usr/local/bin/app"]

运维上看,镜像小了,拉取更快,漏洞扫描面也更少——基础的 golang 编译镜像有几百 MB,最终运行镜像可能只有十几 MB。

健康检查

HEALTHCHECK 指令在容器内周期性执行命令判断服务是否正常:

dockerfile
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
  CMD curl -fsS http://127.0.0.1:8080/health || exit 1

检查命令在容器内部执行。镜像里没有 curl 就会失败,所以要确保检查工具存在于镜像中。健康检查路径要轻,不适合塞复杂数据库查询——处理健康检查本身不应该成为负载。

镜像瘦身的方向

做法说明
使用 slim/alpine/distroless减少基础系统体积
多阶段构建编译依赖不进最终镜像
清理包管理缓存避免 apt/yum 缓存留在层里
.dockerignore排除源码目录里无关文件
固定依赖版本减少不可复现构建

Alpine 很小,但它用的是 musl libc 而不是 glibc。某些程序依赖 glibc 特定行为时,在 Alpine 上会出问题。镜像瘦身不是只看体积数字,线上稳定性比镜像小 20MB 重要。

BuildKit 的缓存挂载和密钥

BuildKit 是当前 Docker 的默认构建后端,支持更好的缓存、secret mount 和 SSH mount。

构建缓存挂载——依赖下载缓存只用于构建过程,不进入最终镜像:

dockerfile
# syntax=docker/dockerfile:1.7
FROM python:3.12-slim
WORKDIR /app

COPY requirements.txt .

# pip 缓存挂载到 /root/.cache/pip,构建完成后不留在镜像里
RUN --mount=type=cache,target=/root/.cache/pip \
    pip install -r requirements.txt

secret mount——构建时需要的 Token、密码不写进 Dockerfile 的 ENVARG

dockerfile
# syntax=docker/dockerfile:1.7
FROM alpine:3.20

# /run/secrets/npm_token 只在这一条 RUN 期间存在,不会进入最终镜像层
RUN --mount=type=secret,id=npm_token \
    cat /run/secrets/npm_token >/dev/null
bash
docker build \
  --secret id=npm_token,src=/opt/secret/npm_token \
  -t registry.example.com/demo/app:1.0 .

SSH mount——构建时需要访问私有 Git 仓库:

dockerfile
# syntax=docker/dockerfile:1.7
FROM alpine/git:2.45

# 使用宿主机 SSH agent,不把私钥 COPY 进镜像
RUN --mount=type=ssh \
    git ls-remote git@example.com:ops/private-repo.git
bash
docker build --ssh default -t registry.example.com/demo/git-check:1.0 .

"密钥不进镜像层"应该当成 Dockerfile 的硬检查项。docker history 里能看到的内容,就不该出现密码、Token、私钥路径和内部仓库凭据。

Dockerfile 检查项

方向检查点
基础镜像是否固定版本,来源是否可信
构建缓存依赖层和代码层顺序是否合理
镜像大小是否清理缓存,是否使用多阶段构建
用户是否默认 root,挂载目录的 UID/GID 是否明确
启动主进程是否前台运行,exec 形式是否正确
密钥是否被 COPY 进镜像,构建凭据是否使用 secret/SSH mount
健康检查检查命令是否存在,路径是否轻量