之前写了篇 Docker 配代理的文章,把 Docker 拉镜像慢的坑给填了。这次来聊聊一个更实际的话题——咋把 Python 项目正经地 Docker 化部署。
说实话,刚开始搞 Docker 的时候,我写的 Dockerfile 那叫一个粗糙,镜像动辄 1G+,构建还贼慢。后来踩了不少坑,才算摸出一套相对靠谱的写法。今天就从基础到进阶,一次性给大伙儿捋清楚。
一个最基础的 Dockerfile
咱先从最简单的来,假设你有个 Python 项目,结构大概长这样:
1 2 3 4 5 6
| my-project/ ├── app/ │ ├── __init__.py │ └── main.py ├── requirements.txt └── Dockerfile
|
最基本的 Dockerfile 写法:
1 2 3 4 5 6 7 8 9 10
| FROM python:3.10-slim
WORKDIR /app
COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["python", "-m", "app.main"]
|
看着挺简单对吧?但这其实就是个”能跑”的版本,离”好用”还差得远。
这里用的是 python:3.10-slim 而不是 python:3.10。slim 版本基于 Debian 精简版,体积小了一大截,一般场景够用了。如果你的项目需要编译 C 扩展,可能还得加上 build-essential。
层缓存优化——别小看这几行顺序
上面的 Dockerfile 里,我 故意把 COPY requirements.txt 放在 COPY . . 前面,这不是随便写的,是有讲究的。
Docker 构建镜像的时候是按层缓存的。如果某一层的输入没变,Docker 就直接用缓存,不会再执行。问题来了——如果你把 COPY . . 放前面,那你改一行代码,pip install 那层就得重新跑,每次构建都得重新下载所有依赖,坑爹不?
所以正确做法是:先 COPY requirements.txt,执行 pip install,再 COPY 整个项目代码。这样只要你没改依赖文件,pip install 那层就会命中缓存,构建速度飞起。
1 2 3 4 5 6
| COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt
COPY . .
|
.dockerignore——别把垃圾塞进镜像
好家伙,之前有一次构建镜像,发现咋这么大?一查才发现,COPY . . 把 .git、__pycache__、venv 全 TM 的拷进去了。
所以项目根目录下一定要加一个 .dockerignore:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| .git .gitignore __pycache__ *.pyc *.pyo venv/ .env .venv/ *.egg-info dist/ build/ .idea/ .vscode/ node_modules/
|
这玩意儿的作用跟 .gitignore 类似,就是告诉 Docker 构建的时候忽略哪些文件。不加 .dockerignore,你的镜像可能凭空大几百 MB。
多阶段构建——进阶玩法
接下来是重头戏。如果你的项目有需要编译的依赖(比如 psycopg2、numpy 之类的),构建阶段需要 build-essential 这些编译工具,但运行的时候根本用不到。多阶段构建就是为了解决这个问题:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| FROM python:3.10-slim AS builder
WORKDIR /app
RUN apt-get update && apt-get install -y --no-install-recommends \ build-essential \ && rm -rf /var/lib/apt/lists/*
RUN python -m venv /opt/venv ENV PATH="/opt/venv/bin:$PATH"
COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt
FROM python:3.10-slim AS runtime
WORKDIR /app
COPY --from=builder /opt/venv /opt/venv ENV PATH="/opt/venv/bin:$PATH"
COPY . .
CMD ["python", "-m", "app.main"]
|
这样搞出来的好处是:最终镜像里没有编译工具链,体积能小一半。build-essential 那一套加起来好几百 MB 呢,运行时根本不需要。
原理很简单:Docker 只会把最后一个阶段打包成镜像,前面阶段的东西用完就扔了。但通过 COPY --from=builder 可以把需要的产物(这里是虚拟环境)拷过来。
踩坑点合集
1. 非 root 用户运行
默认情况下,容器里的进程是以 root 身份跑的。这在生产环境是个安全隐患——万一容器被攻破,攻击者直接就是 root。
1 2 3 4 5
| RUN useradd --create-home --shell /bin/bash appuser
USER appuser
|
把这两行加在 COPY . . 之后、CMD 之前就行。
注意文件权限问题!如果你先 COPY 代码再切换用户,appuser 可能没权限写某些目录。需要提前用 chown 处理好,或者在 COPY 之前就切换用户。
2. 时区问题
容器默认是 UTC 时区,日志时间和咱们差 8 个小时,排查问题的时候看着贼难受。
1 2
| ENV TZ=Asia/Shanghai RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
|
或者更简单,直接设置环境变量让 Python 程序自己处理:
不过建议还是用上面 ln -snf 的方式,这样系统层面的时区也是对的。
3. 代理配置
有些兄弟的服务器网络环境特殊,pip install 走不了公网,得配代理。这个问题我之前那篇文章已经详细讲过了,这里简单提一下:
1 2 3 4
| ARG HTTP_PROXY ARG HTTPS_PROXY RUN pip install --no-cache-dir -r requirements.txt
|
构建的时候这样传:
1 2 3
| docker build --build-arg HTTP_PROXY=http://your-proxy:port \ --build-arg HTTPS_PROXY=http://your-proxy:port \ -t myapp .
|
更详细的 Docker 代理配置(包括 daemon.json 全局代理、镜像加速等),可以看我之前写的这篇文章:Docker 配置代理实战。
docker-compose.yml 示例
光有 Dockerfile 还不够,实际部署一般还得配合 docker-compose。来一个完整的示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| version: "3.8"
services: app: build: context: . dockerfile: Dockerfile container_name: my-python-app restart: unless-stopped ports: - "8000:8000" environment: - TZ=Asia/Shanghai - DATABASE_URL=postgresql://user:pass@db:5432/mydb - REDIS_URL=redis://redis:6379/0 volumes: - ./logs:/app/logs depends_on: - db - redis
db: image: postgres:15-alpine container_name: my-postgres restart: unless-stopped environment: POSTGRES_USER: user POSTGRES_PASSWORD: pass POSTGRES_DB: mydb volumes: - pgdata:/var/lib/postgresql/data ports: - "5432:5432"
redis: image: redis:7-alpine container_name: my-redis restart: unless-stopped ports: - "6379:6379"
volumes: pgdata:
|
几个要点:
restart: unless-stopped 让容器意外退出后自动重启,生产环境必备
volumes 把日志目录挂载出来,方便排查问题,不用每次都 docker exec 进去看
depends_on 控制启动顺序,但要注意它只保证容器启动,不保证服务就绪。如果你的应用对数据库连接有时序要求,代码里还是要做好重连逻辑
最终版 Dockerfile
把上面的知识点整合一下,来一个生产可用的完整版:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| FROM python:3.10-slim AS builder
WORKDIR /app
RUN apt-get update && apt-get install -y --no-install-recommends \ build-essential \ && rm -rf /var/lib/apt/lists/*
RUN python -m venv /opt/venv ENV PATH="/opt/venv/bin:$PATH"
COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt
FROM python:3.10-slim
ENV TZ=Asia/Shanghai RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
WORKDIR /app
COPY --from=builder /opt/venv /opt/venv ENV PATH="/opt/venv/bin:$PATH"
COPY . .
RUN useradd --create-home --shell /bin/bash appuser \ && chown -R appuser:appuser /app USER appuser
EXPOSE 8000
CMD ["python", "-m", "app.main"]
|
总结
回顾一下这篇文章的核心要点:
- 基础镜像选 slim 版,别用完整版,省空间
- requirements.txt 单独 COPY + pip install,利用层缓存加速构建
- .dockerignore 必须有,别把无关文件塞进镜像
- 多阶段构建分离编译和运行环境,减小镜像体积
- 非 root 用户运行,安全第一
- 时区、代理这些坑提前踩好
Docker 化部署这事儿,说难不难,但细节很多。希望这篇文章能帮你少走点弯路,别像我当初一样踩坑踩到怀疑人生。
参考资料