目录

Dockerfile 避坑指南

写 Dockerfile 看似简单,但实际生产中有大量隐蔽的坑。本文整理了常见的 Dockerfile 踩坑场景和对应的解决方案,帮你少走弯路。

坑 1:apt-get update 和 install 分开写

# ❌ 缓存层问题:update 被缓存后,install 可能安装旧版本甚至找不到包
RUN apt-get update
RUN apt-get install -y curl

# ✅ 合并到同一层
RUN apt-get update && apt-get install -y --no-install-recommends curl \
    && rm -rf /var/lib/apt/lists/*

apt-get update 单独一层会被缓存,后续修改 install 的包列表时不会重新执行 update,导致安装失败。


坑 2:COPY . . 放在 RUN install 之前

# ❌ 任何文件改动都会让依赖安装缓存失效
COPY . .
RUN pip install -r requirements.txt

# ✅ 先复制依赖文件,再复制源码
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .

改一行代码就要重新安装所有依赖,构建时间从 30 秒变 5 分钟。


坑 3:用 shell 格式写 ENTRYPOINT

# ❌ shell 格式:进程以 /bin/sh -c 启动,PID 1 是 sh 而不是你的应用
ENTRYPOINT python app.py

# ✅ exec 格式:应用直接作为 PID 1
ENTRYPOINT ["python", "app.py"]

shell 格式的后果:

  • docker stop 发送的 SIGTERM 被 sh 吞掉,应用收不到信号
  • 容器停止时只能等 10 秒超时后被 SIGKILL 强杀
  • 优雅退出(graceful shutdown)完全失效

坑 4:在 RUN 中设置环境变量

# ❌ 每个 RUN 是独立的 shell,变量不会传递
RUN export APP_HOME=/opt/app
RUN cd $APP_HOME  # $APP_HOME 为空

# ✅ 使用 ENV 指令
ENV APP_HOME=/opt/app
RUN cd $APP_HOME && do_something

坑 5:忽略 .dockerignore 导致镜像臃肿

没有 .dockerignore 时,COPY . . 会把所有文件发送到 Docker daemon:

# 常见的被误打包的文件
.git/          # 可能几百 MB
node_modules/  # 本地依赖和容器内架构可能不同
.env           # 包含密钥,严重安全隐患
*.log
dist/

真实案例: 一个 Node.js 项目,源码 5MB,但 .git 目录 800MB,构建上下文传输就要 30 秒。


坑 6:在镜像中硬编码密钥

# ❌ 密钥会永久留在镜像层中,即使后续删除也能通过 docker history 看到
ENV DB_PASSWORD=my_secret_password
RUN echo "password=my_secret_password" > /app/config

# ✅ 运行时注入
# docker run -e DB_PASSWORD=xxx myapp
# 或使用 Docker secrets / Vault

即使你在后面的层 RUN rm /app/config,中间层仍然包含该文件。


坑 7:alpine 镜像的 DNS 和 glibc 问题

# ❌ 某些应用依赖 glibc,alpine 用的是 musl libc
FROM alpine:3.19
COPY myapp /usr/local/bin/
# 运行时报错:/lib/x86_64-linux-gnu/libc.so.6: No such file or directory

解决方案:

  • 编译时静态链接:CGO_ENABLED=0(Go)
  • 换用 debian:bookworm-slimdistroless
  • alpine 下的 DNS 解析在高并发时可能有问题(musl 的 DNS 实现与 glibc 不同)

坑 8:多阶段构建忘记复制运行时依赖

FROM golang:1.22 AS builder
RUN go build -o server .

FROM alpine:3.19
COPY --from=builder /go/src/app/server /usr/local/bin/
# ❌ 如果应用依赖 ca-certificates 或时区数据,运行时会报错
# TLS 请求失败:x509: certificate signed by unknown authority

# ✅ 安装运行时依赖
RUN apk add --no-cache ca-certificates tzdata

常见遗漏:

  • ca-certificates — HTTPS 请求必需
  • tzdata — 时区相关功能必需
  • 动态链接库 — 如果不是静态编译

坑 9:VOLUME 指令的隐式行为

# ⚠️ VOLUME 之后对该目录的修改会被丢弃
VOLUME /data
RUN echo "init" > /data/config  # 这行写入会在运行时被匿名卷覆盖

VOLUME 声明后,后续 RUN 对该路径的写入在容器启动时会被空的匿名卷覆盖。如果需要初始化数据,在 VOLUME 之前完成,或使用 entrypoint 脚本在运行时初始化。


坑 10:构建时的时区和 locale 问题

# ❌ 容器内默认 UTC 时区,日志时间和宿主机不一致
# ❌ locale 缺失导致中文乱码

# ✅ 设置时区
ENV TZ=Asia/Shanghai
RUN apk add --no-cache tzdata

# ✅ Debian 系设置 locale
RUN apt-get update && apt-get install -y locales \
    && locale-gen en_US.UTF-8 \
    && rm -rf /var/lib/apt/lists/*
ENV LANG=en_US.UTF-8

坑 11:docker build 缓存不生效的隐藏原因

缓存失效的常见原因:

  • 文件权限变化: chmod 后文件内容没变,但权限变了,缓存失效
  • 时间戳变化: git clone 后文件 mtime 不同,缓存失效
  • BuildKit 和旧引擎行为不同: DOCKER_BUILDKIT=1 和传统引擎的缓存策略有差异
  • ARG 在 FROM 之前: FROM 之前的 ARG 不会传递到构建阶段
# ❌ ARG 在 FROM 之前声明,FROM 之后不可用
ARG VERSION=3.19
FROM alpine:$VERSION
RUN echo $VERSION  # 空值

# ✅ FROM 之后重新声明
ARG VERSION=3.19
FROM alpine:$VERSION
ARG VERSION
RUN echo $VERSION  # 3.19

坑 12:僵尸进程问题

容器内 PID 1 进程不会自动回收子进程,导致僵尸进程堆积:

# ✅ 使用 tini 作为 init 进程
RUN apk add --no-cache tini
ENTRYPOINT ["tini", "--"]
CMD ["python", "app.py"]

或者在 docker run 时加 --init 参数。