1. 项目概述:为什么一个简单的 Flask API 需要 Docker?
“Docker + Flask | Dockerizing a Python API”——这个标题看起来像教程目录里最不起眼的一行,但在我过去三年带过的27个后端交付项目里,它恰恰是客户踩坑率最高、返工次数最多、上线前最后一刻还在改配置的环节。不是因为 Flask 多难,也不是因为 Docker 多复杂,而是绝大多数人把“容器化”当成一个“打包动作”,而忽略了它本质是一次运行环境契约的重新定义。
我见过太多这样的现场:本地flask run一切正常,pip install -r requirements.txt在服务器上却报ModuleNotFoundError: No module named 'pymysql';开发说“我用的是 Python 3.9.16”,运维查了下服务器 Python 版本是 3.9.18,结果发现pydantic的某个 patch 版本在 3.9.16 和 3.9.18 之间存在隐式依赖冲突;还有更隐蔽的——本地用 SQLite 开发,测试环境切 MySQL,但SQLALCHEMY_DATABASE_URI硬编码在config.py里,一打包进镜像就写死,换环境就得重构建……这些都不是 bug,是环境假设不一致导致的契约失效。
所以,“Dockerizing a Python API”的核心目的,从来不是“让代码跑起来”,而是把“能跑”的全部条件——Python 版本、依赖版本、系统库、时区、文件权限、启动顺序、健康检查逻辑——全部固化成可验证、可审计、可复现的声明式描述。它解决的不是“能不能部署”,而是“部署之后,是否和你本地调试时的行为完全一致”。这正是我在金融类 API 项目中坚持要求所有 Flask 服务必须通过Dockerfile+docker-compose.yml双文件交付的根本原因:监管审计时,我们能直接cat Dockerfile向合规团队证明“该服务仅使用 OpenSSL 1.1.1w,未启用 TLS 1.0,数据库连接池最大数为 20,无任何外部网络外连行为”。
适合谁来读这篇?如果你正面临以下任一场景,这篇文章就是为你写的:
- 你刚写完一个 Flask 接口,准备丢给同事或测试环境,但对方说“跑不起来,缺包”;
- 你用
virtualenv管理依赖,但每次换机器都要重装、重试、重调路径; - 你听说过
docker build,但不清楚FROM python:3.9-slim和FROM python:3.9-alpine到底差在哪,也不敢随便换; - 你想让前端同事本地一键拉起后端服务,而不是教他装 Python、改 host、配
.env; - 你正在写简历,想把“熟悉 Docker”从自我介绍里摘出来,变成能讲清楚
COPY . /app和COPY requirements.txt /app/顺序差异的硬技能。
这不是 Docker 入门课,也不是 Flask 教程。这是从真实交付现场抠出来的、关于“如何让一个轻量级 Python API 在容器里真正稳住”的实操手册。接下来每一节,都对应一个我亲手填平过的坑。
2. 整体设计思路:为什么这样组织 Dockerfile 和项目结构?
2.1 不是“先写代码再容器化”,而是“按容器思维重构项目结构”
很多人的第一反应是:我 Flask 项目已经写好了,现在加个Dockerfile就行。错。容器不是魔法盒,它不会自动修复你项目里埋着的环境耦合。真正的起点,是你得用容器的约束倒逼代码结构升级。
我强制要求所有新启动的 Flask 项目,初始目录结构必须长这样:
my-flask-api/ ├── app/ │ ├── __init__.py # 创建 Flask 实例、注册蓝图、初始化扩展 │ ├── models.py # ORM 模型(如用 SQLAlchemy) │ ├── routes/ │ │ ├── __init__.py │ │ ├── api_v1.py # 主业务路由 │ │ └── health.py # 健康检查路由(独立文件,便于测试) │ └── extensions.py # 所有扩展实例化(db, migrate, cors 等) ├── config/ │ ├── __init__.py │ ├── base.py # 公共配置(SECRET_KEY 占位符、日志格式) │ ├── development.py # 本地开发(DEBUG=True, SQLite) │ ├── testing.py # 单元测试(内存数据库) │ └── production.py # 生产环境(DEBUG=False, PostgreSQL, 严格 CORS) ├── migrations/ # Alembic 迁移文件(由 flask-migrate 生成) ├── tests/ # pytest 测试用例 ├── requirements.txt # 生产依赖(不含 dev-only 包) ├── requirements-dev.txt # 开发依赖(pytest, black, mypy) ├── Dockerfile # 核心构建文件(只 COPY 必需内容) ├── docker-compose.yml # 本地开发与多服务编排 ├── gunicorn.conf.py # Gunicorn 生产启动配置 ├── entrypoint.sh # 启动前预检脚本(可选但强烈推荐) └── README.md看到没?config/是独立目录,app/是纯业务包,requirements.txt明确区分生产/开发。这种结构不是为了“好看”,而是为了精准控制Dockerfile中的COPY操作粒度。比如:
COPY requirements.txt /tmp/→RUN pip install -r /tmp/requirements.txt→COPY . /app/
这个顺序保证:只要requirements.txt没变,Docker 构建缓存就复用已安装的依赖层,极大加速 CI/CD。如果把COPY . /app/放前面,哪怕只改了一个空格,整个 pip install 都要重来。COPY app/ /app/app/而不是COPY . /app/,避免把tests/、.git/、__pycache__/这些非运行时需要的文件打进镜像,减小镜像体积(实测一个 500 行的 Flask 项目,错误 COPY 全局目录会让镜像从 120MB 涨到 380MB)。
提示:
Dockerfile里永远不要出现RUN pip install flask这种硬编码。所有依赖必须收口到requirements.txt,并确保其内容是pip freeze --all > requirements.txt的精确输出(用pip-tools更佳)。否则你会遇到“本地pip install flask==2.3.3,CI 里pip install flask自动装了 2.4.0,结果flask.json模块被移除导致 500 错误”的惨剧。
2.2 为什么放弃flask run,而选择 Gunicorn + Nginx 组合?
新手常问:“Flask 自带flask run,为啥还要搞 Gunicorn?”答案很现实:flask run是调试服务器,不是生产服务器。它的 WSGI 实现基于 Werkzeug 的简易 HTTP 服务器,单线程、无进程管理、无超时控制、不支持 graceful shutdown——这些在生产环境全是致命缺陷。
我拿一个真实压测数据说话:同一台 4C8G 的云服务器,部署同一个用户登录接口(含 JWT 验签、Redis 缓存校验、MySQL 查询),分别用flask run和gunicorn --workers 4 --worker-class sync --timeout 30 --keep-alive 5运行:
| 指标 | flask run(默认) | Gunicorn (4 workers) |
|---|---|---|
| 并发 100 请求平均延迟 | 1280ms | 210ms |
| 最大稳定 QPS | 32 | 287 |
| 内存占用(稳定态) | 110MB | 320MB(4 进程) |
| 进程崩溃后自动恢复 | ❌ 不支持 | ✅ systemd 或 supervisord 可接管 |
关键点在于:Gunicorn 的--workers参数不是越多越好。我实测过,对 CPU 密集型任务(如大量 JSON 解析、加密计算),worker 数 = CPU 核数 + 1 是黄金比例;对 I/O 密集型(如频繁调用外部 API、数据库查询),用--worker-class gevent配合--worker-connections 1000效果更好。但无论哪种,都必须配合--timeout(防慢请求拖垮整个 worker)和--keep-alive(复用 TCP 连接,降低握手开销)。
而 Nginx 的角色,是 Gunicorn 的“守门员”:它处理 SSL 终止、静态文件服务、请求限流(limit_req)、IP 黑白名单、以及最重要的——优雅关闭(graceful shutdown)。当你要滚动更新镜像时,Nginx 会先把新请求导向新容器,等旧容器里所有活跃连接自然结束(由proxy_read_timeout控制),才真正 kill 进程。没有 Nginx,Gunicorn 的--preload和--graceful-timeout再完善,也挡不住客户端 TCP 连接被粗暴中断。
所以我的标准生产栈永远是:
Client → Nginx(SSL/TLS + 负载均衡) → Gunicorn(WSGI Server) → Flask App
Dockerfile里只装 Gunicorn,Nginx 单独起一个容器(通过docker-compose.yml关联),这是解耦的最佳实践。
2.3 为什么docker-compose.yml不是可选项,而是必选项?
有人觉得:“我就一个 Flask 服务,写docker run命令就够了。”这在单机演示时成立,但在真实协作中,它会迅速演变成灾难。docker-compose.yml的价值,远不止于“少打几个命令”。
它解决三个核心问题:
- 环境变量统一注入:
flask run依赖.env,但.env文件不能进 Git(含密钥),也不能进镜像(安全风险)。docker-compose.yml的environment:或env_file:可以把敏感配置从代码中剥离,且支持docker-compose --env-file prod.env up动态切换。 - 多容器依赖编排:你的 Flask API 几乎一定需要数据库、缓存、消息队列。
docker-compose.yml用depends_on+healthcheck定义启动顺序和依赖健康状态。比如:
这段配置确保:Web 容器启动前,PostgreSQL 必须通过services: web: build: . depends_on: db: condition: service_healthy redis: condition: service_healthy db: image: postgres:15 healthcheck: test: ["CMD-SHELL", "pg_isready -U postgres"] interval: 30s timeout: 10s retries: 5pg_isready检查,否则等待重试。没有它,你大概率遇到psycopg2.OperationalError: connection refused。 - 本地开发体验一致性:前端同事
git clone后,只需docker-compose up -d,就能获得和测试环境完全一致的后端服务(包括 DB 版本、Redis 配置、网络隔离),无需在他本机装 PostgreSQL、配pg_hba.conf、开 5432 端口。这才是“开箱即用”。
我坚持所有项目docker-compose.yml必须包含development和production两个 profile,并用profiles:字段隔离。开发版暴露端口、挂载源码、禁用日志轮转;生产版关闭所有调试端口、启用日志收集、设置资源限制。这样,docker-compose --profile production up就是准生产环境,--profile development up就是开发者沙箱——一套配置,两种形态。
3. 核心细节解析:Dockerfile 每一行背后的深意
3.1 基础镜像选择:python:3.9-slimvspython:3.9-alpine,别只看体积
Dockerfile第一行FROM,看似简单,实则定生死。我见过太多人盲目追求“最小镜像”,选了alpine,结果栽在 C 扩展上。
先看数据对比(基于pip install flask sqlalchemy pymysql后的最终镜像大小):
| 基础镜像 | 层级数 | 最终镜像大小 | 是否含 glibc | 兼容性风险点 |
|---|---|---|---|---|
python:3.9-slim | 7 | 128MB | ✅ 是 | 无(Debian 衍生,生态最稳) |
python:3.9-alpine | 5 | 72MB | ❌ 否(musl) | pymysql正常,但cryptography、psycopg2-binary可能因 musl 编译失败 |
alpine的核心问题是musl libc 替代 glibc。虽然大部分纯 Python 包没问题,但一旦涉及 C 扩展(如数据库驱动、加密库、图像处理),就必须重新编译。psycopg2-binary官方 wheel 只提供 glibc 版本,alpine下pip install psycopg2-binary会 fallback 到源码编译,而 Alpine 默认不装gcc、postgresql-dev,导致构建失败。你得手动RUN apk add --no-cache gcc postgresql-dev musl-dev,这不仅增加构建时间,还引入了额外的攻击面(编译工具链不该出现在生产镜像里)。
我的经验法则:
- 新项目、无历史包袱、明确知道所有依赖都支持 musl→ 可选
alpine,但必须做完整依赖兼容性验证(pip install后python -c "import psycopg2; import cryptography")。 - 企业级项目、需快速交付、依赖不确定→ 无脑选
python:3.9-slim。它基于 Debian slim,体积比 full 版小 60%,又保留了完整的 glibc 兼容性,是稳字当头的最优解。
注意:
slim版本不包含apt-get以外的包管理器(如yum),也不含vim、curl等调试工具。这反而是优点——生产镜像越“瘦”,攻击面越小。调试时用docker exec -it <container> sh进入,临时装curl查问题即可,不必把调试工具塞进镜像。
3.2 多阶段构建(Multi-stage Build):如何把 1.2GB 的构建镜像压缩到 128MB?
这是 Docker 最被低估的特性之一。很多人Dockerfile长这样:
FROM python:3.9-slim WORKDIR /app COPY requirements.txt . RUN pip install -r requirements.txt # 这里会装 200+ 个包,含构建期依赖 COPY . . CMD ["gunicorn", "--config", "gunicorn.conf.py", "app:create_app()"]问题在哪?pip install时,setuptools、wheel、pip自身,以及一些需要编译的包(如cryptography)会下载大量.tar.gz源码、.so临时文件、__pycache__,它们全留在最终镜像里。docker history <image>一看,光pip install这一层就占 300MB。
多阶段构建的精髓,在于分离构建环境和运行环境:
# 构建阶段:完整环境,装所有需要的工具和依赖 FROM python:3.9-slim AS builder WORKDIR /app COPY requirements.txt . # 安装构建期依赖(如 gcc, python3-dev) RUN apt-get update && apt-get install -y --no-install-recommends \ gcc \ python3-dev \ && rm -rf /var/lib/apt/lists/* RUN pip install --user --no-warn-script-location -r requirements.txt # 运行阶段:极简环境,只复制构建产物 FROM python:3.9-slim WORKDIR /app # 复制 builder 阶段安装的包到用户 site-packages COPY --from=builder /root/.local/bin/ /usr/local/bin/ COPY --from=builder /root/.local/lib/python3.9/site-packages/ /usr/local/lib/python3.9/site-packages/ # 复制应用代码 COPY app/ /app/app/ COPY config/ /app/config/ COPY gunicorn.conf.py /app/gunicorn.conf.py COPY entrypoint.sh /app/entrypoint.sh RUN chmod +x /app/entrypoint.sh CMD ["/app/entrypoint.sh"]关键点解析:
--from=builder让第二阶段能精准引用第一阶段的文件系统,不污染最终镜像。COPY --from=builder /root/.local/...复制的是--user安装的路径,比系统级/usr/local/lib更干净,避免覆盖基础镜像的系统包。- 最终镜像里只有
python3.9运行时、gunicorn可执行文件、app/代码、config/,以及entrypoint.sh。没有apt、没有gcc、没有.tar.gz缓存。实测一个中型 Flask 项目,镜像体积从 380MB 降到 128MB,推送 Registry 时间减少 65%。
实操心得:
requirements.txt里一定要区分build-time和run-time依赖。比如black、mypy、pytest这些开发工具,绝不能进生产镜像。我用pip-tools管理:pip-compile requirements.in --output-file requirements.txt,requirements.in只放运行时依赖,dev-requirements.in单独管理开发依赖。
3.3entrypoint.sh:不只是启动脚本,更是运行前的“体检医生”
很多人把启动逻辑全塞CMD里,比如CMD ["gunicorn", "--config", "gunicorn.conf.py", "app:create_app()"]。这很危险——如果gunicorn.conf.py里bind地址写错了,或者app:create_app()报错,容器会立即退出,你连日志都看不到。
entrypoint.sh的价值,在于把启动拆成“预检”和“执行”两步,让失败可诊断:
#!/bin/sh set -e # 任何命令失败,立即退出 echo "[INFO] Starting pre-flight checks..." # 检查必要环境变量 if [ -z "$DATABASE_URL" ]; then echo "[ERROR] DATABASE_URL is not set. Please provide via environment variable." exit 1 fi # 检查数据库连接(用 pg_isready 或 mysqladmin) if [ "$DB_TYPE" = "postgres" ]; then if ! pg_isready -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -t 5; then echo "[ERROR] Cannot connect to PostgreSQL at $DB_HOST:$DB_PORT" exit 1 fi elif [ "$DB_TYPE" = "mysql" ]; then if ! mysqladmin ping -h "$DB_HOST" -P "$DB_PORT" -u "$DB_USER" -p"$DB_PASSWORD" --silent; then echo "[ERROR] Cannot connect to MySQL at $DB_HOST:$DB_PORT" exit 1 fi fi # 检查 migrations 是否最新(针对 SQLAlchemy + Flask-Migrate) if [ -n "$FLASK_APP" ] && [ -f "/app/migrations/versions/" ]; then if ! python -c "from app import create_app; app = create_app(); from flask_migrate import upgrade; upgrade()" 2>/dev/null; then echo "[WARN] Database migrations may be out of date. Run 'flask db upgrade' manually." fi fi echo "[INFO] All pre-flight checks passed. Starting Gunicorn..." exec "$@"这个脚本做了三件事:
- 强校验环境变量:
DATABASE_URL是刚需,缺失直接exit 1,容器停止,避免 Gunicorn 启动后报一堆晦涩的OperationalError。 - 真连数据库:不是 ping 端口,而是用数据库原生命令(
pg_isready/mysqladmin)验证服务可达性和认证有效性。这能提前暴露网络策略、密码错误、SSL 配置等问题。 - 迁移状态快照:尝试执行
upgrade(),不强制成功(用|| true也可),但至少告诉你“当前代码期望的表结构和数据库实际结构是否一致”。
exec "$@"是关键——它用exec替换当前 shell 进程,让 Gunicorn 成为 PID 1。这保证了信号(如SIGTERM)能正确传递给 Gunicorn,实现优雅关闭。如果不用exec,shell 是 PID 1,Gunicorn 是子进程,docker stop发的SIGTERM会被 shell 拦截,Gunicorn 收不到,导致强制 kill。
注意:
entrypoint.sh必须用#!/bin/sh,不是#!/bin/bash。Alpine 和 slim 镜像默认只有sh,bash需额外安装,违背“最小化”原则。所有语法必须兼容 POSIX sh(比如用[ -z "$VAR" ]而不是[[ -z $VAR ]])。
4. 实操过程:从零构建一个可交付的 Flask API 容器
4.1 初始化项目与配置:create_app()工厂函数的正确写法
我们以一个真实的用户管理 API 为例,逐步构建。首先,app/__init__.py的create_app()必须是工厂模式,且支持配置注入:
# app/__init__.py import os from flask import Flask from flask_sqlalchemy import SQLAlchemy from flask_migrate import Migrate from flask_cors import CORS from config import config_by_name db = SQLAlchemy() migrate = Migrate() cors = CORS() def create_app(config_name=None): if config_name is None: config_name = os.getenv('FLASK_ENV', 'development') app = Flask(__name__) app.config.from_object(config_by_name[config_name]) # 初始化扩展 db.init_app(app) migrate.init_app(app, db) cors.init_app(app, resources={r"/api/*": {"origins": app.config.get("CORS_ORIGINS", "*")}}) # 注册蓝图 from app.routes.api_v1 import bp as api_v1_bp app.register_blueprint(api_v1_bp, url_prefix='/api/v1') from app.routes.health import bp as health_bp app.register_blueprint(health_bp, url_prefix='/health') return app重点看config_by_name[config_name]—— 它从config/目录动态加载配置,而非硬编码。config/base.py定义公共项:
# config/base.py import os class Config: SECRET_KEY = os.environ.get('SECRET_KEY') or 'dev-secret-key-change-in-prod' SQLALCHEMY_TRACK_MODIFICATIONS = False # 日志配置 LOG_LEVEL = os.environ.get('LOG_LEVEL', 'INFO') # CORS 配置(生产环境应限制域名) CORS_ORIGINS = os.environ.get('CORS_ORIGINS', '*')config/production.py则覆盖关键项:
# config/production.py from config.base import Config class ProductionConfig(Config): DEBUG = False TESTING = False # 数据库 URL 从环境变量读取,绝不硬编码 SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') # Gunicorn 日志路径(需挂载卷) LOG_FILE = '/var/log/myapi/app.log' # 生产环境 CORS 严格限制 CORS_ORIGINS = ['https://myfrontend.com', 'https://admin.myfrontend.com']为什么这么设计?因为Dockerfile里COPY config/进镜像,但DATABASE_URL这种敏感信息,必须由docker-compose.yml注入。这样,镜像本身是通用的,环境差异全由配置驱动。
4.2Dockerfile完整实现:附带注释的生产级模板
以下是经过 12 个项目验证的Dockerfile,每行都有明确意图:
# 使用多阶段构建,第一阶段为 builder FROM python:3.9-slim AS builder # 设置工作目录 WORKDIR /app # 复制 requirements.txt(注意:只复制这一文件,利用 Docker 缓存) COPY requirements.txt . # 安装构建期依赖:gcc 用于编译 C 扩展,python3-dev 提供头文件 RUN apt-get update && apt-get install -y --no-install-recommends \ gcc \ python3-dev \ && rm -rf /var/lib/apt/lists/ # 使用 --user 安装到 /root/.local,避免污染系统 site-packages RUN pip install --user --no-warn-script-location -r requirements.txt # 第二阶段:运行时环境 FROM python:3.9-slim # 创建非 root 用户(安全最佳实践) RUN groupadd -g 1001 -f appuser && useradd -r -u 1001 -g appuser appuser USER appuser # 设置工作目录 WORKDIR /app # 复制 builder 阶段安装的包(注意路径匹配) COPY --from=builder /root/.local/bin/ /usr/local/bin/ COPY --from=builder /root/.local/lib/python3.9/site-packages/ /usr/local/lib/python3.9/site-packages/ # 复制应用代码(只复制必需目录,排除测试、文档等) COPY app/ /app/app/ COPY config/ /app/config/ COPY gunicorn.conf.py /app/gunicorn.conf.py COPY entrypoint.sh /app/entrypoint.sh # 设置执行权限 RUN chmod +x /app/entrypoint.sh # 暴露端口(仅声明,不实际打开) EXPOSE 8000 # 启动前预检 + 执行 Gunicorn ENTRYPOINT ["/app/entrypoint.sh"] CMD ["gunicorn", "--config", "gunicorn.conf.py", "app:create_app()"]关键安全加固点:
USER appuser:禁止以 root 运行,防止容器内提权攻击。EXPOSE 8000:只是文档化声明,实际端口映射由docker run -p或docker-compose.yml控制。ENTRYPOINT+CMD分离:ENTRYPOINT固定为预检脚本,CMD可被docker run覆盖(如docker run ... /bin/sh进入调试),灵活性高。
4.3gunicorn.conf.py:生产环境的核心参数调优
gunicorn.conf.py不是摆设,它是性能和稳定性的开关:
# gunicorn.conf.py import multiprocessing # 绑定地址和端口(必须和 entrypoint.sh 里检查的端口一致) bind = "0.0.0.0:8000" bind_address = "0.0.0.0:8000" port = "8000" # Unix socket 更高效,但 Docker 内部用 TCP 更简单 # bind = "unix:/tmp/gunicorn.sock" # umask = 007 # 工作进程配置 workers = multiprocessing.cpu_count() * 2 + 1 worker_class = "sync" # I/O 密集型可换 "gevent" worker_connections = 1000 max_requests = 1000 max_requests_jitter = 100 # 超时设置(防慢请求拖垮) timeout = 30 keepalive = 5 graceful_timeout = 30 # 日志配置(Docker 日志驱动会捕获 stdout/stderr) accesslog = "-" # 输出到 stdout,供 docker logs 查看 errorlog = "-" # 输出到 stderr loglevel = "info" access_log_format = '%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s" %(D)s' # 进程命名 proc_name = "my-flask-api" # 安全相关 daemon = False pidfile = "/tmp/gunicorn.pid" # 限制内存,防 OOM(单位:MB) # limit_request_field_size = 8190 # limit_request_line = 0 # limit_request_fields = 100参数详解:
workers = multiprocessing.cpu_count() * 2 + 1:这是经验公式。CPU 核数为 2 时,开 5 个 worker;4 核开 9 个。过多 worker 会争抢 CPU,过少则无法充分利用多核。timeout = 30:单个请求处理超过 30 秒,Gunicorn 强制 kill worker 进程,防雪崩。graceful_timeout = 30:收到SIGTERM后,等待 30 秒让 worker 处理完当前请求再退出。必须 ≥timeout。accesslog = "-":-表示 stdout,Docker 会自动收集,docker logs <container>即可查看,无需挂载日志卷。
实操心得:
gunicorn.conf.py里的bind地址必须是0.0.0.0:8000,不能是127.0.0.1:8000。因为容器内127.0.0.1指向容器自身,而 Nginx 在另一个容器,它需要通过 Docker 网络访问web:8000,所以必须监听所有接口。
4.4docker-compose.yml:本地开发与生产部署的同一份声明
这是docker-compose.yml的生产就绪版,含开发和生产 profile:
version: '3.8' services: web: build: context: . dockerfile: Dockerfile image: my-flask-api:latest # 仅开发 profile 启用 profiles: ["development"] ports: - "5000:8000" # 本地访问 http://localhost:5000 environment: - FLASK_ENV=development - DATABASE_URL=postgresql://postgres:password@db:5432/myapi - LOG_LEVEL=DEBUG volumes: # 开发时热重载代码(生产环境禁用!) - ./app:/app/app:ro - ./config:/app/config:ro depends_on: db: condition: service_healthy redis: condition: service_healthy # 生产 profile 的 web 服务(无 volume,固定镜像 tag) web-prod: image: my-flask-api:1.2.0 profiles: ["production"] # 生产环境不暴露端口,由 Nginx 反向代理 expose: - "8000" environment: - FLASK_ENV=production - DATABASE_URL=${DATABASE_URL} - REDIS_URL=${REDIS_URL} - LOG_LEVEL=WARNING # 资源限制,防单容器吃光宿主机 deploy: resources: limits: memory: 512M cpus: '0.5' depends_on: db-prod: condition: service_healthy redis-prod: condition: service_healthy # PostgreSQL 数据库(开发版) db: image: postgres:15 profiles: ["development"] environment: - POSTGRES_DB=myapi - POSTGRES_USER=postgres - POSTGRES_PASSWORD=password volumes: - postgres_data:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U postgres -d myapi"] interval: 30s timeout: 10s retries: 5 # 生产版数据库(可对接云 RDS) db-prod: image: postgres:15 profiles: ["production"] environment: - POSTGRES_DB=myapi # 生产环境不挂载卷,用云数据库 healthcheck: test: ["CMD-SHELL", "pg_isready -U ${DB_USER} -d ${DB_NAME} -h ${DB_HOST} -p ${DB_PORT}"] interval: 30s timeout: 10s retries: 5 # Redis 缓存 redis: image: redis:7-alpine profiles: ["development", "production"] command: redis-server --appendonly yes healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 30s timeout: 10s retries: 5 volumes: postgres_data:使用方式:
- 本地开发:
docker-compose --profile development up -d - 生产部署:
docker-compose --profile production --env-file prod.env up -d,其中prod.env包含DATABASE_URL=postgresql://user:pass@rds-host:5432/db等。
注意:
volumes:在生产 profile 中被禁用,因为云数据库不需要本地卷。deploy.resources是 Swarm 模式下的资源限制,即使不用 Swarm,Docker Compose v2.15+ 也支持docker compose up时生效。
5. 常见问题与排查技巧实录:那些年我填过的坑
5.1 问题速查表:高频故障与定位路径
| 现象 | 可能原因 | 快速定位命令 | 解决方案 | |