Docker容器化部署的踩坑实录
Docker容器化部署的踩坑实录
把应用"装进盒子"很容易,让它稳定跑在生产环境则是另一回事。
大三下学期接手实验室一个老项目,导师一句话:"把它容器化吧,方便部署。"我心想,这有何难?docker build、 docker run,十分钟搞定。
两个月后,生产环境三次宕机、两次数据丢失、一次镜像膨胀到 8GB——我才意识到:Docker 入门只需一天,踩坑能踩一学期。
坑一:镜像层乱写,体积失控
最初写的 Dockerfile 长这样:
FROM ubuntu:20.04
RUN apt-get update
RUN apt-get install -y python3 python3-pip nodejs npm
RUN pip3 install flask pandas numpy scikit-learn
RUN pip3 install -r requirements.txt
COPY . /app
WORKDIR /app
CMD ["python3", "app.py"]
构建完一看:docker images 显示 3.2GB。就一个 Flask API,为什么要 3GB?
问题出在层(Layer)设计上:apt-get 的缓存、pip 的缓存、中间产物全部被打进镜像。更糟糕的是,COPY . /app 把 .git、__pycache__、测试数据全部塞进去了。
改进版:
FROM python:3.11-slim
# 合并 RUN 指令,减少层数;清理缓存
RUN pip install --no-cache-dir flask pandas numpy && \
rm -rf /var/lib/apt/lists/*
# 先只复制依赖声明,利用构建缓存
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# 最后复制应用代码
COPY --chown=appuser:appuser . /app
WORKDIR /app
USER appuser
CMD ["gunicorn", "--bind", "0.0.0.0:8080", "app:app"]
关键改进点:
1. 用 python:3.11-slim 替代 ubuntu:20.04,基础镜像从 1.2GB 降到 180MB
2. 合并 RUN 指令,减少层数
3. pip --no-cache-dir 禁止缓存
4. 先 COPY requirements.txt 再安装,这样只有依赖变更时才重新执行 pip install,大幅加速日常构建
5. 加 .dockerignore 排除无用文件
最终镜像体积:286MB,缩减到原来的 9%。
坑二:忽略 .dockerignore,密钥泄露险些发生
这是最危险的一个坑。某次构建时没注意,COPY . /app 把本地 .env 文件(内含数据库密码和 JWT 密钥)打进了镜像。
更可怕的是,这个镜像后来被 docker push 到了公共仓库。
虽然及时删除了,但公共镜像仓库不保证即时清除——别人可能已经拉取了。
此后我的项目根目录必放 .dockerignore:
.git
.gitignore
.env
*.log
__pycache__
*.pyc
node_modules
tests/
*.md
.dockerignore
教训:镜像不是打包工具,它是分发产物。进去的每一行都可能被看见。
坑三:进程前台运行问题(PID 1 陷阱)
用 Docker 跑 Java 应用时,发现 docker stop 发 SIGTERM 后,容器要等 10 秒才退出(Docker 默认等待 10 秒后发 SIGKILL)。
原因是:Java 进程默认不处理 SIGTERM,收到信号后不会优雅退出,只能等超时后被强制杀死。这会导致:
- 数据库连接未正常关闭
- 正在处理的请求被中断
- 文件写入不完整
解决方案,在 docker-compose.yml 中加 init: true:
services:
app:
build: .
init: true # 使用 tini 作为 PID 1,负责信号转发
environment:
- JAVA_OPTS=-XX:+ExitOnOutOfMemoryError -Djava.awt.headless=true
ports:
- "8080:8080"
或者在 Dockerfile 里直接装 tini:
RUN apt-get update && apt-get install -y --no-install-recommends tini
ENTRYPOINT ["/usr/bin/tini", "--"]
CMD ["java", "-jar", "app.jar"]
坑四:时区问题,日志时间全错
Docker 容器默认使用 UTC 时区。某次排查线上 Bug,对着日志找了半天,后来发现日志时间是 UTC,而我本地是 CST(东八区),差了 8 小时,白白浪费一上午。
解决方式,在 Dockerfile 里设置时区:
ENV TZ=Asia/Shanghai
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
或者在 docker-compose.yml 里挂载本地时区文件:
volumes:
- /etc/localtime:/etc/localtime:ro
- /etc/timezone:/etc/timezone:ro
坑五:数据持久化,误用容器文件系统
刚开始用 Docker 跑 MySQL,数据存在容器内部。某次更新镜像,执行了 docker-compose down && docker-compose up -d,所有数据没了。
Docker 容器的文件系统是临时的,容器删除后数据随之消失。正确做法是用 Volume 或 bind mount:
services:
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: ${DB_PASSWORD}
volumes:
- mysql_data:/var/lib/mysql # 命名卷,数据持久化
- ./mysql/init.sql:/docker-entrypoint-initdb.d/init.sql:ro
ports:
- "3306:3306"
volumes:
mysql_data: # 声明命名卷
原则:凡是有状态的数据(数据库、上传文件、日志),一律放在 Volume 里。
坑六:网络连通性,容器间通信踩雷
Docker Compose 多服务场景下,最容易出问题的就是网络。
# 错误写法:用 localhost 访问另一个容器
# app.py 里写:requests.get("http://localhost:3306")
# 正确写法:用服务名作为 hostname
# requests.get("http://mysql:3306")
容器内的 localhost 指的是当前容器自身,不是宿主机,也不是其他容器。Docker 内置 DNS 会把服务名解析为对应容器的 IP。
建议显式声明网络:
networks:
app_net:
driver: bridge
services:
app:
networks:
- app_net
mysql:
networks:
- app_net
总结:Docker 不是银弹
容器化确实解决了"在我机器上能跑"的问题,但它引入的复杂性不容忽视:
| 问题 | 影响 | 解决方式 |
|---|---|---|
| 镜像体积大 | 构建慢、传输慢、存储贵 | 多阶段构建 + .dockerignore |
| 进程信号处理 | 优雅关闭失效 | tini / init: true |
| 时区不一致 | 日志时间错乱 | ENV TZ + ln -sf |
| 数据丢失 | 生产事故 | Volume 持久化 |
| 网络不通 | 服务调用失败 | 服务名 + 自定义网络 |
我现在的 Dockerfile 模板,已经迭代了四版,每次踩坑都往里加一条规则。容器化不是终点,让应用稳定、可复现、易维护地运行才是目的。
如果你也刚开始用 Docker,希望这些坑能帮你少走点弯路。
(写于 2026 年夏,大三即将结束之际)

浙公网安备 33010602011771号