Docker 容器化部署:编写 Dockerfile 与 Docker Compose 编排

📂 所属阶段:第五阶段 — 工程化与部署(实战篇)
🔗 相关章节:Nginx + Uvicorn 生产部署 · Pydantic Settings 多环境配置


1. 为什么需要 Docker?

1.1 问题:环境不一致

开发机器:Python 3.11, PostgreSQL 13, Redis 7
测试服务器:Python 3.10, PostgreSQL 14, Redis 6
生产服务器:Python 3.11, PostgreSQL 15, Redis 7

"在我电脑上明明能跑!" 😤

1.2 Docker 解决一切

项目代码 + Dockerfile → Docker Image → Container(无论在哪都完全一致)

2. Dockerfile 编写

2.1 基础 Dockerfile

# Dockerfile(多阶段构建)
# 阶段一:构建
FROM python:3.11-slim as builder

WORKDIR /app
RUN pip install --no-cache-dir poetry

COPY pyproject.toml poetry.lock* ./
RUN poetry config virtualenvs.create false \
    && poetry install --no-interaction --no-ansi --no-root

COPY . .

# 阶段二:运行
FROM python:3.11-slim

WORKDIR /app

# 从 builder 复制已安装的依赖
COPY --from=builder /usr/local/lib/python3.11/site-packages /usr/local/lib/python3.11/site-packages
COPY --from=builder /usr/local/bin /usr/local/bin
COPY --from=builder /app /app

# 创建非 root 用户(安全最佳实践)
RUN useradd --create-home appuser
USER appuser

EXPOSE 8000

# 健康检查
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
    CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"

CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

2.2 更简洁的单阶段 Dockerfile

# 轻量级部署(开发/测试)
FROM python:3.11-slim

WORKDIR /app

# 先复制依赖文件,安装完成后再复制代码(利用 Docker 缓存)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

# 生产:使用 gunicorn 多 worker
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

2.3 requirements.txt

fastapi>=0.109.0
uvicorn[standard]>=0.27.0
sqlalchemy[asyncio]>=2.0
asyncpg
redis[hiredis]
python-jose[cryptography]
passlib[bcrypt]
pydantic-settings
python-multipart
httpx
pydantic[email]

3. Docker Compose 编排

3.1 本地开发环境

# docker-compose.yml
version: "3.9"

services:
  api:
    build:
      context: .
      dockerfile: Dockerfile
    container_name: daoman_api
    ports:
      - "8000:8000"
    environment:
      - ENV=development
      - DATABASE_URL=postgresql+asyncpg://postgres:postgres@db:5432/myapp
      - REDIS_URL=redis://redis:6379/0
      - JWT_SECRET=dev-secret-change-in-production
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_started
    volumes:
      - ./app:/app
    command: uvicorn main:app --host 0.0.0.0 --port 8000 --reload

  db:
    image: postgres:16-alpine
    container_name: daoman_db
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
      POSTGRES_DB: myapp
    ports:
      - "5432:5432"
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      timeout: 5s
      retries: 5

  redis:
    image: redis:7-alpine
    container_name: daoman_redis
    ports:
      - "6379:6379"
    volumes:
      - redis_data:/data

volumes:
  postgres_data:
  redis_data:

3.2 生产环境 Docker Compose

# docker-compose.prod.yml
version: "3.9"

services:
  api:
    build:
      context: .
      dockerfile: Dockerfile.prod
    image: daoman_api:latest
    container_name: daoman_api
    restart: always
    expose:
      - "8000"
    env_file:
      - .env.production
    environment:
      - ENV=production
      - DATABASE_URL=${DATABASE_URL}
      - REDIS_URL=${REDIS_URL}
      - JWT_SECRET=${JWT_SECRET}
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_started
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
      interval: 30s
      timeout: 10s
      retries: 3

  db:
    image: postgres:16-alpine
    restart: always
    volumes:
      - postgres_prod_data:/var/lib/postgresql/data
    environment:
      POSTGRES_DB: myapp
      POSTGRES_USER: ${DB_USER}
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${DB_USER}"]
      interval: 10s
      timeout: 5s
      retries: 5

  redis:
    image: redis:7-alpine
    restart: always
    command: redis-server --appendonly yes
    volumes:
      - redis_prod_data:/data

  nginx:
    image: nginx:alpine
    container_name: daoman_nginx
    restart: always
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
      - ./nginx/conf.d:/etc/nginx/conf.d:ro
      - ./nginx/ssl:/etc/nginx/ssl:ro
    depends_on:
      - api

volumes:
  postgres_prod_data:
  redis_prod_data:

4. 生产构建与部署

4.1 构建镜像

# 本地构建
docker build -t daoman_api:latest .

# 带 BuildKit 加速(推荐)
DOCKER_BUILDKIT=1 docker build -t daoman_api:latest .

# 打标签推送到仓库
docker tag daoman_api:latest yourregistry.com/daoman_api:v1.0.0
docker push yourregistry.com/daoman_api:v1.0.0

4.2 生产部署

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

# 重启服务(零停机)
docker-compose -f docker-compose.prod.yml up -d --no-deps --build api

# 查看日志
docker-compose -f docker-compose.prod.yml logs -f api

# 进入容器调试
docker exec -it daoman_api /bin/sh

5. 小结

Docker 部署流程:
1. 写 Dockerfile(多阶段构建)
2. 写 docker-compose.yml(本地开发)
3. 写 docker-compose.prod.yml(生产)
4. docker build 构建镜像
5. docker-compose up -d 启动服务
6. 配置 Nginx 反向代理

💡 最佳实践:使用非 root 用户运行容器、定期重建镜像清理漏洞、用 Healthcheck 实现容器自愈。


🔗 扩展阅读