Docker容器化部署的踩坑实录

Docker容器化部署的踩坑实录

把应用"装进盒子"很容易,让它稳定跑在生产环境则是另一回事。

大三下学期接手实验室一个老项目,导师一句话:"把它容器化吧,方便部署。"我心想,这有何难?docker builddocker 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 stopSIGTERM 后,容器要等 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 容器的文件系统是临时的,容器删除后数据随之消失。正确做法是用 Volumebind 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 年夏,大三即将结束之际)

posted @ 2026-07-06 14:56  C(5,3)  阅读(4)  评论(0)    收藏  举报