Appearance
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.txtsecret mount——构建时需要的 Token、密码不写进 Dockerfile 的 ENV 或 ARG:
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/nullbash
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.gitbash
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 |
| 健康检查 | 检查命令是否存在,路径是否轻量 |