写一个生产级 Dockerfile

能跑起来的 Dockerfile 五分钟就能写出来,难的是三件事:镜像要小、构建要快、进程要能优雅退出。多阶段构建解决体积,依赖清单先复制解决缓存,exec 形式解决信号传递。本章按这三条主线把指令逐条讲清,并给出 Go、Python、Node 三份可直接改用的模板。总原则一句话:镜像越小越好,但别为了几十 MB 牺牲可调试性——alpine 省体积,却常常缺 glibc、缺字体、连 ps 都没有。

指令逐条解释

指令作用生产要点
FROM指定基础镜像固定具体版本号,禁用 latest;构建与运行阶段分开
RUN构建时执行命令多条命令用 && 合并,并在同层清理缓存
COPY复制文件进镜像先排除无用文件;依赖清单单独一行好利用缓存
ENV / ARG环境变量与构建参数ARG 值会留在镜像历史里,不要用它传密钥
USER切换运行用户非 root 运行,安全硬要求
CMD / ENTRYPOINT启动命令与固定入口用 exec 数组形式,见本章倒数第二节

多阶段构建:Go

FROM golang:1.23-bookworm AS builder
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
    go build -trimpath -ldflags "-s -w -X main.version=1.4.0" -o /out/app ./cmd/app
FROM alpine:3.20
# 时区与中文字体:乱码与 8 小时时差都是缺这两个包
RUN apk add --no-cache ca-certificates tzdata font-noto-cjk \
 && ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
ENV TZ=Asia/Shanghai LANG=C.UTF-8
WORKDIR /app
COPY --from=builder /out/app /app/app
RUN adduser -D -u 10001 appuser && chown -R appuser:appuser /app
USER appuser
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
  CMD wget -qO- http://127.0.0.1:8000/healthz || exit 1
ENTRYPOINT ["/app/app"]

多阶段构建:Python

FROM python:3.12-slim AS builder
WORKDIR /app
RUN apt-get update && apt-get install -y --no-install-recommends build-essential \
 && rm -rf /var/lib/apt/lists/*
COPY requirements.txt .
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt

FROM python:3.12-slim
RUN apt-get update && apt-get install -y --no-install-recommends tzdata fonts-noto-cjk \
 && ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && rm -rf /var/lib/apt/lists/*
ENV TZ=Asia/Shanghai PYTHONUNBUFFERED=1 PYTHONDONTWRITEBYTECODE=1
COPY --from=builder /install /usr/local
WORKDIR /app
COPY . .
RUN useradd -u 10001 -m appuser && chown -R appuser:appuser /app
USER appuser
CMD ["gunicorn", "app.main:app", "-k", "uvicorn.workers.UvicornWorker", \
     "-w", "2", "-b", "0.0.0.0:8000", "--access-logfile", "-"]

多阶段构建:Node(构建产物交给 Nginx)

FROM node:20-bookworm-slim AS builder
WORKDIR /app
RUN corepack enable
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile
COPY . .
RUN pnpm build

FROM nginx:1.27-alpine
RUN apk add --no-cache tzdata && ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
COPY --from=builder /app/dist /usr/share/nginx/html

.dockerignore:上下文越小构建越快

.git
node_modules
.venv
dist
logs
*.log
.env*
*.pem

node_modules.venv 不排除,构建上下文可能从几 MB 涨到几百 MB;.env 与证书更要排除,否则会被打进镜像层。

层缓存与构建顺序

Docker 的每一层都带缓存,某层输入变了,它和它之后的所有层全部失效,因此变化越少的放越前面——先复制依赖清单再安装依赖,就是这个原则最直接的应用。

顺序内容变化频率
1FROM 基础镜像极低
2系统依赖 apt-get install、依赖清单、安装依赖低到中
3源码 COPY . . 与构建
docker build --platform linux/amd64 -t myapi:1.4.0 .
# Apple Silicon 上构建给 x86 服务器用的镜像,必须显式指定平台
docker buildx build --platform linux/amd64,linux/arm64 -t myapi:1.4.0 --push .

CMD 与 ENTRYPOINT 的区别与信号传递

CMD gunicorn app:app -b 0.0.0.0:8000      # 反例:shell 形式,信号收不到
CMD ["gunicorn", "app:app", "-b", "0.0.0.0:8000"]   # 正例:exec 形式
组合行为结果
ENTRYPOINT ["app"] + CMD ["--port=8000"]参数可被覆盖推荐写法
CMD ["app"] 单独用整条命令可被覆盖简单容器够用
两者都用 shell 形式都经过 sh信号丢失,坚决避免

docker stop 先给 PID 1 发 SIGTERM,等 10 秒(默认)后 SIGKILL。PID 1 若是 shell,信号不会转发,正在处理的请求直接被砍,用户看到 502。

验证方法

docker images myapi:1.4.0 --format 'table {{.Repository}}\t{{.Tag}}\t{{.Size}}'
docker run -d --name t --rm -p 127.0.0.1:18000:8000 myapi:1.4.0
sleep 30 && docker inspect t --format '{{.State.Health.Status}}'   # 期望 healthy
docker exec -it t id                                               # 期望 uid=10001
time docker stop t                                                 # 期望 1~2 秒退出

常见坑

现象做法
FROM python:3 浮动 tag今天能构建,明天报错固定到 3.12-slim
没配 .dockerignore构建慢、镜像里混入 .env排除依赖目录与密钥
COPY . . 在装依赖之前改一行代码就重装全部依赖先复制依赖清单再安装
ENV 里写密钥镜像历史可见运行期用 -e 或 secrets
只装 slim 不装 tzdata/fonts时间差 8 小时、中文变方框显式安装 tzdatafonts-noto-cjk

小结:Dockerfile 的三条主线是体积、速度、信号——多阶段构建把编译环境与运行环境分开,依赖清单先复制让层缓存尽量复用,exec 形式的 CMD/ENTRYPOINT 让进程收到 SIGTERM 优雅退出;固定基础镜像版本、配好 .dockerignore、非 root 运行、装齐 tzdata 与中文字体,才有资格叫生产级。

笔记加载中…