Docker 部署全流程:Dockerfile 与 Docker Compose 编排

📂 所属阶段:第六阶段 — 上线部署(生产篇)
🔗 相关章节:Gunicorn 与 Nginx · 环境变量与安全配置


开发 Flask 项目时,你是不是也遇到过这种头疼的事:本地用 Python 3.11、Redis 7、PostgreSQL 16 跑完全正常,上传到云服务器,环境版本对不上、服务依赖缺漏、路径配置崩了?

Docker 就是解决「环境一致性地狱」的神器——把整个应用(包括依赖、配置、基础环境)打包成「镜像」,再由镜像生成「容器」,不管在哪台机器上,容器内的运行状态都完全一致。

这篇文章带大家走一遍从编写 Dockerfile 构建 Flask 镜像,到用 Docker Compose 本地开发、生产部署的全流程。


1. 写好一份高效安全的 Dockerfile

Dockerfile 是构建 Docker 镜像的「说明书」,每一行指令都会生成一个「镜像层」——合理利用分层缓存能大幅加速构建,同时注意非 root 用户等安全细节。

完整代码

# Dockerfile
# 选 Python 3.11 的 slim 版(不是 full 版):体积小,只保留运行必要的系统库,适合生产
FROM python:3.11-slim

# 切换工作目录:后续所有命令都在 /app 下执行,避免路径混乱
WORKDIR /app

# 先复制依赖文件!!!这是分层缓存的核心优化
# 只要 requirements.txt 没变,后面的 pip install 就会直接复用已有的镜像层,不用每次改代码都重新下依赖
COPY requirements.txt .
# --no-cache-dir 禁用 pip 本地缓存,进一步减小镜像体积
RUN pip install --no-cache-dir -r requirements.txt

# 最后复制应用代码:代码频繁修改,放最下面能最小化重建的镜像层
COPY . .

# 安全优化:创建非 root 用户运行容器
# 容器默认用 root 权限,如果被入侵,攻击者可能操控宿主机;非 root 能大幅降低风险
RUN useradd --create-home appuser && \
    chown -R appuser:appuser /app
USER appuser

# 声明容器暴露的端口:只是个说明,不做实际映射(映射靠 docker run 或 docker-compose)
EXPOSE 8000

# 启动命令:用 Gunicorn 生产级 WSGI 服务器,不要用 flask run(仅适合开发)
CMD ["gunicorn", "-c", "gunicorn.conf.py", "app:create_app()"]

配套 requirements.txt

包含了常用 Flask 全家桶、PostgreSQL/Redis 驱动、异步任务 Celery 等:

# requirements.txt
Flask>=3.0.0
gunicorn>=21.0.0
flask-sqlalchemy>=3.1.0
flask-login>=0.6.3
flask-wtf>=1.2.0
flask-migrate>=4.0.0
flask-cors>=4.0.0
psycopg2-binary>=2.9.0 # PostgreSQL 二进制驱动,不用编译
redis>=5.0.0
python-dotenv>=1.0.0
email-validator>=2.0.0
Pillow>=10.0.0
celery>=5.3.0

2. 用 Docker Compose 编排多容器

一个完整的 Flask 生产项目,往往不止 Web 服务——还有数据库、Redis 缓存/消息队列、Nginx 反向代理等。单独用 docker run 启动太麻烦,Docker Compose 用一个 YAML 文件就能管理所有容器的依赖、网络、存储、环境变量

2.1 本地开发版:热重载、简化配置

本地开发时,我们需要:

  • 代码修改后自动重载(不用重启容器)
  • 环境变量直接写在 YAML 里(不用单独 .env
  • 数据库和 Redis 带数据持久化(但可以随项目删除)
# docker-compose.yml
version: "3.9" # 稳定常用的版本

services: # 定义所有容器
  # Flask Web 服务
  web:
    build: . # 基于当前目录的 Dockerfile 构建镜像
    container_name: daoman_web # 自定义容器名,方便 exec/logs
    ports:
      - "8000:8000" # 宿主机8000 → 容器8000
    environment: # 开发环境变量
      - FLASK_ENV=development
      - DATABASE_URL=postgresql://postgres:postgres@db:5432/daoman
      - REDIS_URL=redis://redis:6379/0
    volumes: # 挂载本地代码到容器,实现热重载
      - ./app:/app
    depends_on: # 依赖关系:web 必须等 db 和 redis 健康/启动后再启动
      db:
        condition: service_healthy
      redis:
        condition: service_started
    command: flask run --host=0.0.0.0 # 覆盖 Dockerfile 的 CMD,用开发服务器

  # PostgreSQL 数据库
  db:
    image: postgres:16-alpine # 同样选轻量的 alpine 版
    container_name: daoman_db
    environment: # 初始化数据库的默认配置
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
      POSTGRES_DB: daoman
    volumes: # 数据持久化:挂载到宿主机的 Docker 卷(不是本地目录)
      - postgres_data:/var/lib/postgresql/data
    healthcheck: # 健康检查:只有数据库能连了才叫健康
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      timeout: 5s
      retries: 5

  # Redis 缓存/消息队列
  redis:
    image: redis:7-alpine
    container_name: daoman_redis

  # Celery 异步任务 Worker
  celery:
    build: .
    container_name: daoman_celery
    command: celery -A app.celery_app worker --loglevel=info
    environment:
      - DATABASE_URL=postgresql://postgres:postgres@db:5432/daoman
      - REDIS_URL=redis://redis:6379/0
    depends_on:
      - db
      - redis

volumes: # 声明宿主机的 Docker 卷,跨容器重启数据不会丢
  postgres_data:

2.2 生产环境版:安全、稳定、持久化

生产环境和本地开发的核心区别:

  • 不用热重载,用预构建好的镜像(或 CI/CD 推送的)
  • 环境变量放单独的 .env.production(敏感信息不能提交代码)
  • 所有容器加 restart: always(崩溃自动重启)
  • Web 服务加健康检查
  • 用 Nginx 做反向代理、静态文件服务、SSL 证书配置
  • 所有数据持久化,Redis 开启 AOF 持久化
# docker-compose.prod.yml
version: "3.9"

services:
  # Flask Web 服务
  web:
    image: daoman/flask:latest # 直接用预构建的镜像(比如 Docker Hub 或私有仓库的)
    container_name: daoman_web
    restart: always # 崩溃、重启宿主机后自动启动
    env_file: # 读取单独的生产环境变量
      - .env.production
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_started
    healthcheck: # Web 服务健康检查:访问 /health 接口,确保服务正常
      test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
      interval: 30s
      timeout: 10s
      retries: 3

  # PostgreSQL 数据库
  db:
    image: postgres:16-alpine
    restart: always
    volumes:
      - postgres_prod:/var/lib/postgresql/data
    env_file:
      - .env.production

  # Redis 缓存/消息队列
  redis:
    image: redis:7-alpine
    restart: always
    command: redis-server --appendonly yes # 开启 AOF 持久化,数据更安全
    volumes:
      - redis_prod:/data

  # Nginx 反向代理
  nginx:
    image: nginx:alpine
    restart: always
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro # ro = 只读,安全
      - ./nginx/ssl:/etc/nginx/ssl:ro
      - static_files:/var/www/static # 挂载 Web 服务的静态文件目录
    depends_on:
      - web

volumes:
  postgres_prod:
  redis_prod:
  static_files:

3. 一键构建与部署流程

本地开发

# 1. 启动所有服务(-d = 后台运行)
docker-compose up -d

# 2. 查看 Web 服务日志(-f = 实时跟随)
docker-compose logs -f web

# 3. 修改代码后会自动热重载,刷新浏览器即可

# 4. 停止服务(数据还在 Docker 卷里)
docker-compose down

# 5. 停止并删除所有数据(开发完清理用)
docker-compose down -v

云服务器生产部署

假设你已经把镜像推送到 Docker Hub(或私有仓库),服务器上安装好了 Docker 和 Docker Compose,并且上传了 .env.productionnginx/ 目录:

# 1. 拉取最新的镜像
docker-compose -f docker-compose.prod.yml pull

# 2. 启动所有服务(首次部署)
docker-compose -f docker-compose.prod.yml up -d

# 3. 如果只更新了 Web 镜像,只重启 Web 服务(--no-deps = 不重启依赖的 db/redis)
docker-compose -f docker-compose.prod.yml up -d --no-deps --build web

# 4. 进入容器调试(比如看代码、执行 flask db upgrade)
docker exec -it daoman_web /bin/sh

4. 关键最佳实践回顾

  1. Dockerfile 分层优化:先复制依赖文件,再复制应用代码,最大化利用镜像层缓存
  2. 安全降权:容器内用非 root 用户运行
  3. 数据持久化:数据库、Redis、静态文件都挂载到 Docker 卷
  4. 健康检查:生产环境所有服务加健康检查,确保依赖启动后再运行
  5. 敏感信息隔离:生产环境变量放单独的 .env.production,不要提交代码
  6. 轻量镜像:优先用 -slim-alpine 版本的基础镜像

🔗 扩展阅读