
1. 项目背景业务场景食光集市订单服务的部署历史是一部环境不一致的血泪史第一幕开发小周在 macOS 上用pip install uvicorn0.30.0一切正常。测试环境是 CentOS 7Python 3.9 自带的pip版本太老解析依赖链花了 6 分钟最后报ERROR: No matching distribution found for uvicorn0.30.0——CentOS 的 glibc 版本太老不支持。第二幕小周把database.yml数据库密码写进了配置文件然后提交到了 Git。运维在部署前手动替换密码有次替换错了集成测试环境的密码导致测试任务把生产库的订单表清空了——一条DELETE FROM orders WHERE statuscancelled在 prod 库跑了 12 秒才被 DBA 发现。紧急恢复数据耗时 3 小时。第三幕UI 团队新来的设计师需要在本地跑起后端服务做联调。他按照 README 的步骤装 Python 3.13 → 装 Postgres → 装 Redis → 配环境变量 → pip install → 数据库迁移。折腾了整整一上午最后放弃了——“能不能给我一个 docker compose up 就全搞定的”CTO 说“这是’在我机器上能跑’综合症的晚期症状。治疗方案是一个原则同一镜像走遍开发、测试、预发、生产四个环境。”痛点没有容器化和配置管理团队会持续遭遇环境漂移开发环境的numpy1.26CI 镜像里是numpy2.0——API 全变了CI 红一片但本地绿一片。配置散落数据库密码、API 密钥散落在代码、环境变量、配置文件、启动脚本中——审计时不知道哪些机器上有哪些密钥。依赖地狱操作系统级依赖libpq-dev、gcc、openssl和 Python 级依赖psycopg2、cryptography互相捆绑——换一台机器就要重新适配。部署靠人脑运维小李的部署手册是 38 条 Slack 历史消息的截图——新人入职根本部署不起来。2. 项目设计场景测试库被清空的事故复盘会上运维小李红着眼睛说我明明替换的是 test 环境的密码。大师把白板转过来开始画图。小胖看着白板上的 Docker 架构图“大师Docker 我懂——就是把应用和依赖打包成一个镜像嘛。但我的困惑是——Dockerfile 怎么写才算’生产级’我见过有人写FROM python:3.13有人用python:3.13-slim还有python:3.13-alpine——这三种有啥区别”小白“我记得 alpine 镜像特别小只有 8MB。但有人说它的 musl libc 兼容性不好——某些 Python C 扩展如numpy、psycopg2在 alpine 上需要编译特别慢而且运行时偶尔 segfault。这是真的吗另外——Dockerfile 里的RUN、CMD、ENTRYPOINT的区别我永远记不住。”大师“两个问题分别涉及镜像选型和 Dockerfile 最佳实践——这是容器化的第一课。”基础镜像选择python:3.13~900MB完整 Debian包含编译工具链。适合开发调试不适合生产。python:3.13-slim~150MB精简版 Debian只有运行时依赖。生产首选——体积适中兼容性好。python:3.13-alpine~50MB基于 musl libc。体积最小但 C 扩展需要从源码编译apk add build-base耗时。次选——适合纯 Python 项目。选型结论用-slim作为默认体积和兼容性的最优平衡点。有特殊体积要求再考虑 alpine。Dockerfile 指令区别RUN在构建时执行命令如pip install结果固化在镜像层中。CMD在容器启动时执行的默认命令可被docker run ... command覆盖。ENTRYPOINT容器的固定入口点不会被docker run的参数覆盖除非--entrypoint强改。组合推荐ENTRYPOINT [uvicorn]CMD [src.main:app, --host, 0.0.0.0, --port, 8000]——这样用户可以用docker run ... --port 9000只覆盖端口。技术映射基础镜像选型 超市买半成品菜——slim 是净菜洗好切好直接下锅alpine 是原材料新鲜但需要自己处理。RUN 做菜工序固化在菜谱里CMD 上菜方式可以换成外卖盒。小胖“那多阶段构建是干什么的我看有人把编译步骤和运行步骤分开——镜像从 800MB 减到 150MB这是怎么做到的”大师“多阶段构建multi-stage build解决了’编译期依赖污染运行期镜像’的问题。Python 项目的典型场景——你需要gcc、libpq-dev来编译psycopg2但运行时不需要这些编译工具。那就分两个阶段”# 阶段 1构建阶段大而全 FROM python:3.13-slim AS builder RUN apt-get update apt-get install -y gcc libpq-dev COPY requirements.txt . RUN pip install --user -r requirements.txt # 阶段 2运行阶段只复制构建产物 FROM python:3.13-slim COPY --frombuilder /root/.local /root/.local COPY src/ /app/src/ ENV PATH/root/.local/bin:$PATH CMD [uvicorn, src.main:app]技术映射多阶段构建 搬家策略——打包时用大货车把所有东西装上builder 镜像到新家只卸下生活必需品runtime 镜像货车不留下占地方。小胖“那配置管理呢之前数据库密码泄露的事——怎么才能既让应用读到配置又不把密码写进代码或镜像”小白“12-Factor App 原则里有一条——‘配置与代码分离’。我觉得 Docker 的环境变量 .env文件是一种方案但环境变量也有一个问题——如果容器里有人能执行env命令所有密码全暴露了。有没有更安全的方案”大师配置管理的三个层次——从基础到进阶Level 1环境变量基础os.environ.get(DB_PASSWORD)——最简单的方案但敏感值会出现在docker inspect和 crash dump 中。Level 2Docker Secrets进阶Swarm/K8s 模式下secret 挂载为文件/run/secrets/db_password应用读取文件内容——不在环境变量中暴露不写入镜像层。Level 3Vault/AWS Secrets Manager企业级动态获取、自动轮换、审计日志。适合合规要求高的场景。对于食光集市当前规模——Level 1 pydantic-settings 管理第 14 章已学是最佳平衡点开发用.env文件CI 用 GitHub Secrets 注入环境变量生产用 K8s ConfigMap/Secret。3. 项目实战食光集市订单服务容器化环境准备依赖版本说明Python3.13.14基准版本Docker26容器运行时Docker Composev2多容器编排mkdirfoodmarket-ch23cdfoodmarket-ch23 python-mvenv .venv .venv\Scripts\activate分步实现步骤1编写生产级 DockerfileDockerfile# ── 构建阶段安装依赖 ── FROM python:3.13-slim AS builder WORKDIR /build COPY requirements.txt . RUN pip install --user --no-cache-dir -r requirements.txt # ── 运行阶段最小化镜像 ── FROM python:3.13-slim # 创建非 root 用户 RUN groupadd -r appuser useradd -r -g appuser appuser WORKDIR /app # 从构建阶段复制已编译的依赖 COPY --frombuilder /root/.local /home/appuser/.local # 复制应用代码 COPY src/ /app/src/ COPY pyproject.toml /app/ # 环境变量 ENV PATH/home/appuser/.local/bin:$PATH ENV PYTHONUNBUFFERED1 ENV PYTHONDONTWRITEBYTECODE1 # 切换到非 root 用户 USER appuser # 健康检查 HEALTHCHECK --interval30s --timeout5s --retries3 \ CMD python -c import urllib.request; urllib.request.urlopen(http://localhost:8000/health) EXPOSE 8000 CMD [uvicorn, src.main:app, --host, 0.0.0.0, --port, 8000]requirements.txtfastapi0.115.0 uvicorn0.34.0src/main.pyFastAPI 最小服务——用于 Docker 演示importosfromfastapiimportFastAPI appFastAPI(title食光集市订单服务)app.get(/health)asyncdefhealth():return{status:healthy,env:os.getenv(APP_ENV,unknown),host:os.getenv(HOSTNAME,unknown),}步骤2编写 Docker ComposeAPI Postgres Rediscompose.ymlservices:api:build:.ports:-8000:8000environment:-APP_ENVdevelopment-DB_HOSTpostgres-DB_PORT5432-DB_NAMEfoodmarket-DB_USERfmuser-DB_PASSWORDfmpass-REDIS_URLredis://redis:6379/0depends_on:postgres:condition:service_healthyredis:condition:service_healthyhealthcheck:test:[CMD,python,-c,import urllib.request; urllib.request.urlopen(http://localhost:8000/health)]interval:15stimeout:5sretries:3postgres:image:postgres:16-alpineenvironment:-POSTGRES_DBfoodmarket-POSTGRES_USERfmuser-POSTGRES_PASSWORDfmpassvolumes:-pgdata:/var/lib/postgresql/datahealthcheck:test:[CMD-SHELL,pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB]interval:10stimeout:5sretries:5redis:image:redis:7-alpinevolumes:-redisdata:/datahealthcheck:test:[CMD,redis-cli,ping]interval:5stimeout:3sretries:5volumes:pgdata:redisdata:步骤3实现 pydantic-settings 配置管理src/config.py配置管理——pydantic-settings 从环境变量 / .env 读取frompydantic_settingsimportBaseSettings,SettingsConfigDictclassSettings(BaseSettings):model_configSettingsConfigDict(env_file.env,env_file_encodingutf-8,case_sensitiveFalse,)# 应用app_env:strdevelopmentapp_port:int8000# 数据库db_host:strlocalhostdb_port:int5432db_name:strfoodmarketdb_user:strfmuserdb_password:strpropertydefdatabase_url(self)-str:returnfpostgresql://{self.db_user}:{self.db_password}{self.db_host}:{self.db_port}/{self.db_name}# Redisredis_url:strredis://localhost:6379/0settingsSettings().env.example提交到 GitAPP_ENVdevelopment DB_HOSTlocalhost DB_PORT5432 DB_NAMEfoodmarket DB_USERfmuser DB_PASSWORDchange_me REDIS_URLredis://localhost:6379/0步骤4编写测试tests/test_config.pyimportosimportpytestfromsrc.configimportSettingsdeftest_defaults():默认值测试settingsSettings()assertsettings.app_envdevelopmentassertsettings.db_port5432deftest_from_environment(monkeypatch):环境变量覆盖测试monkeypatch.setenv(APP_ENV,production)monkeypatch.setenv(DB_HOST,prod-db.internal)settingsSettings()assertsettings.app_envproductionassertsettings.db_hostprod-db.internaldeftest_database_url_property():settingsSettings(db_useradmin,db_passwordsecret,db_host10.0.0.1,db_port5432,db_namefoodmarket,)expectedpostgresql://admin:secret10.0.0.1:5432/foodmarketassertsettings.database_urlexpected运行pipinstallpydantic-settingsdockercompose up-d# 启动全套服务curlhttp://localhost:8000/health# 验证python-mpytest tests/-v# 运行测试dockercompose down# 清理完整代码清单foodmarket-ch23/ ├── Dockerfile ├── compose.yml ├── .env.example ├── requirements.txt ├── pyproject.toml ├── src/ │ ├── main.py │ └── config.py ├── tests/ │ └── test_config.py4. 项目总结优点 缺点维度Docker Compose纯 venv 本地跑K8s无容器手动部署环境一致性★★★★★★★★★★★★★★启动速度★★★★★★★★★★★★★★学习曲线★★★★★★★★★★★★★★★资源开销★★★★★★★★★★★★★★★适合团队规模小-中个人中-大不推荐适用场景开发环境一键启动docker compose up——新人 5 分钟跑起全套服务。CI/CD 中跑集成测试在 GitHub Actions 中用docker compose起 Postgres Redis API 跑全链路测试。微服务编排每个服务独立容器Compose 管理依赖关系和网络。跨平台交付同一个镜像在 macOS、Windows、Linux 上行为一致。生产部署的蓝本Compose 文件可直接转换为 K8s YAML 或 Helm Chart。不适用场景极简单文件脚本一个.py文件跑定时任务不需要容器化。需要 GUI 的应用Docker 不擅长图形界面应用。注意事项镜像层的顺序很重要把不常变的命令放前面如apt-get install常变的放后面如COPY src/——最大化利用 Docker 缓存。.dockerignore是必须的至少排除.venv/、__pycache__/、.git/、*.pyc否则上下文大小爆炸。不要在 Dockerfile 里写latest标签FROM python:3.13而非FROM python:latest——避免某天拉到一个不兼容的新版本。secrets不写入镜像COPY .env /app/.env会把密码固化在镜像层中用docker history可以查看。生产环境不要用--reloaduvicorn 的 reload 模式会 fork 子进程在容器内管理复杂且不安全。常见踩坑经验故障案例1容器内 Python 找不到已安装的包现象docker build成功docker run后ModuleNotFoundError: No module named uvicorn。根因Dockerfile 中pip install用的是 root 用户安装到/root/.local运行时切换了USER appuserPATH 中没有那个路径。修复ENV PATH/home/appuser/.local/bin:$PATH并确保pip install --user安装到 appuser 的 home 目录或用多阶段构建从 builder 的/root/.local复制。故障案例2Windows 换行符导致容器内脚本exec format error现象一个entrypoint.sh脚本在 Windows 上编辑后挂载到容器报exec format error。根因Windows 的 CRLF 换行符在 Linux 容器内#!/bin/sh不识别。修复.gitattributes中设置*.sh text eollf或在 Linux CI 中跑dos2unix。故障案例3docker compose up后 API 启动失败因为数据库还没就绪现象API 容器先于 Postgres 容器完全启动数据库连接失败。根因depends_on只等容器启动进程存在不等服务就绪端口可接受连接。修复在compose.yml加healthcheckdepends_on加condition: service_healthyCompose v2.1或在应用入口脚本中加入 wait-for-it 逻辑。思考题CMD的两种写法——CMD [uvicorn, src.main:app]exec 形式和CMD uvicorn src.main:appshell 形式——在信号处理上有什么区别哪种更适合生产环境Docker Compose 的环境变量优先级environment字段、.env文件、shell环境变量——三者同时存在时哪个值最终生效答案见中级篇综合实战章附录。延伸阅读与资源Python 3实战精进从脚本到高并发订单引擎MongoDB 实战进阶与内核修炼python入门Rquests从菜鸟脚本到企业级SDK的网络实战圣经Milvus向量数据库实战修炼从 0 到 1精通向量检索与生产落地后端工程师的 AI 转型第一课Ollama 与私有化大模型实战10倍开发者的 Dify 魔法书从零构建全栈 AI 应用后端工程师转型AI第一课-Ollama 与私有化大模型实战大型语言模型(LLM) vLLM 高性能推理落地实战Agent开发之LlamaIndex 实战修炼与源码进阶大语言模型Transformers 实战修炼与源码剖析