029-容器化部署

难度:🔴 | 预计时间:120分钟 | 前置:028-微服务架构

容器化部署是现代应用开发和部署的标准实践。本章将详细介绍如何使用Docker对FastAPI应用进行容器化,包括单容器部署、多容器编排、生产环境优化等内容。

29.1 Docker基础

29.1.1 Docker环境配置

# docker_utils.py
import docker
import logging
from typing import Dict, List, Optional
from pathlib import Path

class DockerManager:
    """Docker管理器"""
    
    def __init__(self):
        self.client = docker.from_env()
        self.logger = logging.getLogger(__name__)
    
    def build_image(self, 
                   dockerfile_path: str, 
                   tag: str, 
                   build_args: Optional[Dict[str, str]] = None) -> bool:
        """构建Docker镜像"""
        try:
            self.logger.info(f"开始构建镜像: {tag}")
            
            # 构建镜像
            image, logs = self.client.images.build(
                path=dockerfile_path,
                tag=tag,
                buildargs=build_args or {},
                rm=True,
                forcerm=True
            )
            
            # 输出构建日志
            for log in logs:
                if 'stream' in log:
                    self.logger.info(log['stream'].strip())
            
            self.logger.info(f"镜像构建成功: {tag}")
            return True
            
        except Exception as e:
            self.logger.error(f"镜像构建失败: {e}")
            return False
    
    def run_container(self, 
                     image: str, 
                     name: str,
                     ports: Optional[Dict[str, int]] = None,
                     environment: Optional[Dict[str, str]] = None,
                     volumes: Optional[Dict[str, Dict[str, str]]] = None,
                     detach: bool = True) -> Optional[str]:
        """运行容器"""
        try:
            self.logger.info(f"启动容器: {name}")
            
            container = self.client.containers.run(
                image=image,
                name=name,
                ports=ports or {},
                environment=environment or {},
                volumes=volumes or {},
                detach=detach,
                remove=False
            )
            
            self.logger.info(f"容器启动成功: {container.id}")
            return container.id
            
        except Exception as e:
            self.logger.error(f"容器启动失败: {e}")
            return None
    
    def stop_container(self, container_name: str) -> bool:
        """停止容器"""
        try:
            container = self.client.containers.get(container_name)
            container.stop()
            self.logger.info(f"容器已停止: {container_name}")
            return True
        except Exception as e:
            self.logger.error(f"停止容器失败: {e}")
            return False
    
    def remove_container(self, container_name: str) -> bool:
        """删除容器"""
        try:
            container = self.client.containers.get(container_name)
            container.remove(force=True)
            self.logger.info(f"容器已删除: {container_name}")
            return True
        except Exception as e:
            self.logger.error(f"删除容器失败: {e}")
            return False
    
    def get_container_logs(self, container_name: str) -> str:
        """获取容器日志"""
        try:
            container = self.client.containers.get(container_name)
            logs = container.logs(tail=100).decode('utf-8')
            return logs
        except Exception as e:
            self.logger.error(f"获取日志失败: {e}")
            return ""
    
    def list_containers(self, all_containers: bool = False) -> List[Dict]:
        """列出容器"""
        try:
            containers = self.client.containers.list(all=all_containers)
            return [
                {
                    'id': container.id[:12],
                    'name': container.name,
                    'image': container.image.tags[0] if container.image.tags else 'unknown',
                    'status': container.status,
                    'ports': container.ports
                }
                for container in containers
            ]
        except Exception as e:
            self.logger.error(f"列出容器失败: {e}")
            return []
    
    def list_images(self) -> List[Dict]:
        """列出镜像"""
        try:
            images = self.client.images.list()
            return [
                {
                    'id': image.id[:12],
                    'tags': image.tags,
                    'size': image.attrs['Size'],
                    'created': image.attrs['Created']
                }
                for image in images
            ]
        except Exception as e:
            self.logger.error(f"列出镜像失败: {e}")
            return []

29.1.2 Dockerfile模板生成器

# dockerfile_generator.py
from typing import Dict, List, Optional
from pathlib import Path
from jinja2 import Template

class DockerfileGenerator:
    """Dockerfile生成器"""
    
    def __init__(self):
        self.base_template = """
# {{ description }}
FROM {{ base_image }}

# 设置工作目录
WORKDIR {{ workdir }}

# 设置环境变量
{% for key, value in env_vars.items() %}
ENV {{ key }}={{ value }}
{% endfor %}

# 安装系统依赖
{% if system_packages %}
RUN apt-get update && apt-get install -y \
{% for package in system_packages %}
    {{ package }}{% if not loop.last %} \{% endif %}
{% endfor %}
    && rm -rf /var/lib/apt/lists/*
{% endif %}

# 复制依赖文件
{% for src, dest in copy_files.items() %}
COPY {{ src }} {{ dest }}
{% endfor %}

# 安装Python依赖
{% if requirements_file %}
RUN pip install --no-cache-dir --upgrade pip && \
    pip install --no-cache-dir -r {{ requirements_file }}
{% endif %}

# 复制应用代码
COPY {{ app_source }} {{ app_dest }}

# 创建非root用户
{% if create_user %}
RUN useradd --create-home --shell /bin/bash {{ username }} && \
    chown -R {{ username }}:{{ username }} {{ workdir }}
USER {{ username }}
{% endif %}

# 健康检查
{% if health_check %}
HEALTHCHECK --interval={{ health_check.interval }} \
            --timeout={{ health_check.timeout }} \
            --start-period={{ health_check.start_period }} \
            --retries={{ health_check.retries }} \
    CMD {{ health_check.command }}
{% endif %}

# 暴露端口
{% for port in expose_ports %}
EXPOSE {{ port }}
{% endfor %}

# 启动命令
{% if entrypoint %}
ENTRYPOINT {{ entrypoint }}
{% endif %}
{% if cmd %}
CMD {{ cmd }}
{% endif %}
        """.strip()
    
    def generate_fastapi_dockerfile(self, 
                                  config: Dict,
                                  output_path: Optional[str] = None) -> str:
        """生成FastAPI应用的Dockerfile"""
        
        # 默认配置
        default_config = {
            'description': 'FastAPI Application',
            'base_image': 'python:3.11-slim',
            'workdir': '/app',
            'env_vars': {
                'PYTHONUNBUFFERED': '1',
                'PYTHONDONTWRITEBYTECODE': '1'
            },
            'system_packages': ['gcc', 'curl'],
            'copy_files': {
                'requirements.txt': '.'
            },
            'requirements_file': 'requirements.txt',
            'app_source': '.',
            'app_dest': '.',
            'create_user': True,
            'username': 'app',
            'health_check': {
                'interval': '30s',
                'timeout': '30s',
                'start_period': '5s',
                'retries': '3',
                'command': 'curl -f http://localhost:8000/health || exit 1'
            },
            'expose_ports': [8000],
            'cmd': '["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]'
        }
        
        # 合并配置
        merged_config = {**default_config, **config}
        
        # 生成Dockerfile内容
        template = Template(self.base_template)
        dockerfile_content = template.render(**merged_config)
        
        # 保存到文件
        if output_path:
            Path(output_path).write_text(dockerfile_content)
        
        return dockerfile_content
    
    def generate_multi_stage_dockerfile(self, 
                                      stages: List[Dict],
                                      output_path: Optional[str] = None) -> str:
        """生成多阶段构建的Dockerfile"""
        
        dockerfile_lines = []
        
        for i, stage in enumerate(stages):
            stage_name = stage.get('name', f'stage{i}')
            base_image = stage['base_image']
            
            # 阶段开始
            if i == 0:
                dockerfile_lines.append(f"FROM {base_image} AS {stage_name}")
            else:
                dockerfile_lines.append(f"\nFROM {base_image} AS {stage_name}")
            
            # 工作目录
            if 'workdir' in stage:
                dockerfile_lines.append(f"WORKDIR {stage['workdir']}")
            
            # 环境变量
            if 'env_vars' in stage:
                for key, value in stage['env_vars'].items():
                    dockerfile_lines.append(f"ENV {key}={value}")
            
            # 系统包安装
            if 'system_packages' in stage:
                packages = ' \\
    '.join(stage['system_packages'])
                dockerfile_lines.append(
                    f"RUN apt-get update && apt-get install -y \\
    {packages} \\
    && rm -rf /var/lib/apt/lists/*"
                )
            
            # 复制文件
            if 'copy_files' in stage:
                for src, dest in stage['copy_files'].items():
                    dockerfile_lines.append(f"COPY {src} {dest}")
            
            # 从其他阶段复制
            if 'copy_from' in stage:
                for copy_item in stage['copy_from']:
                    from_stage = copy_item['from']
                    src = copy_item['src']
                    dest = copy_item['dest']
                    dockerfile_lines.append(f"COPY --from={from_stage} {src} {dest}")
            
            # 运行命令
            if 'run_commands' in stage:
                for cmd in stage['run_commands']:
                    dockerfile_lines.append(f"RUN {cmd}")
            
            # 用户设置
            if stage.get('create_user'):
                username = stage.get('username', 'app')
                workdir = stage.get('workdir', '/app')
                dockerfile_lines.append(
                    f"RUN useradd --create-home --shell /bin/bash {username} && \\"
                    f"    chown -R {username}:{username} {workdir}"
                )
                dockerfile_lines.append(f"USER {username}")
            
            # 健康检查(仅最后阶段)
            if i == len(stages) - 1 and 'health_check' in stage:
                hc = stage['health_check']
                dockerfile_lines.append(
                    f"HEALTHCHECK --interval={hc['interval']} \\"
                    f"            --timeout={hc['timeout']} \\"
                    f"            --start-period={hc['start_period']} \\"
                    f"            --retries={hc['retries']} \\"
                    f"    CMD {hc['command']}"
                )
            
            # 暴露端口(仅最后阶段)
            if i == len(stages) - 1 and 'expose_ports' in stage:
                for port in stage['expose_ports']:
                    dockerfile_lines.append(f"EXPOSE {port}")
            
            # 启动命令(仅最后阶段)
            if i == len(stages) - 1:
                if 'entrypoint' in stage:
                    dockerfile_lines.append(f"ENTRYPOINT {stage['entrypoint']}")
                if 'cmd' in stage:
                    dockerfile_lines.append(f"CMD {stage['cmd']}")
        
        dockerfile_content = '\n'.join(dockerfile_lines)
        
        # 保存到文件
        if output_path:
            Path(output_path).write_text(dockerfile_content)
        
        return dockerfile_content

29.2 单容器部署

29.2.1 基础Dockerfile

# Dockerfile
FROM python:3.11-slim

# 设置环境变量
ENV PYTHONUNBUFFERED=1 \
    PYTHONDONTWRITEBYTECODE=1 \
    PIP_NO_CACHE_DIR=1 \
    PIP_DISABLE_PIP_VERSION_CHECK=1

# 设置工作目录
WORKDIR /app

# 安装系统依赖
RUN apt-get update && apt-get install -y \
    gcc \
    curl \
    && rm -rf /var/lib/apt/lists/*

# 复制依赖文件
COPY requirements.txt .

# 安装Python依赖
RUN pip install --no-cache-dir --upgrade pip && \
    pip install --no-cache-dir -r requirements.txt

# 复制应用代码
COPY . .

# 创建非root用户
RUN useradd --create-home --shell /bin/bash app && \
    chown -R app:app /app
USER app

# 健康检查
HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 \
    CMD curl -f http://localhost:8000/health || exit 1

# 暴露端口
EXPOSE 8000

# 启动命令
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

29.2.2 优化的生产环境Dockerfile

# Dockerfile.prod
FROM python:3.11-slim as builder

# 设置环境变量
ENV PYTHONUNBUFFERED=1 \
    PYTHONDONTWRITEBYTECODE=1 \
    PIP_NO_CACHE_DIR=1 \
    PIP_DISABLE_PIP_VERSION_CHECK=1

# 安装构建依赖
RUN apt-get update && apt-get install -y \
    gcc \
    g++ \
    && rm -rf /var/lib/apt/lists/*

# 设置工作目录
WORKDIR /app

# 复制依赖文件
COPY requirements.txt .

# 安装Python依赖到临时目录
RUN pip install --user --no-cache-dir --upgrade pip && \
    pip install --user --no-cache-dir -r requirements.txt

# 生产阶段
FROM python:3.11-slim as production

# 设置环境变量
ENV PYTHONUNBUFFERED=1 \
    PYTHONDONTWRITEBYTECODE=1 \
    PATH=/home/app/.local/bin:$PATH

# 安装运行时依赖
RUN apt-get update && apt-get install -y \
    curl \
    && rm -rf /var/lib/apt/lists/* \
    && apt-get clean

# 创建应用用户
RUN useradd --create-home --shell /bin/bash app

# 设置工作目录
WORKDIR /app

# 从构建阶段复制Python包
COPY --from=builder /root/.local /home/app/.local

# 复制应用代码
COPY --chown=app:app . .

# 切换到应用用户
USER app

# 健康检查
HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 \
    CMD curl -f http://localhost:8000/health || exit 1

# 暴露端口
EXPOSE 8000

# 启动命令
CMD ["gunicorn", "main:app", "-w", "4", "-k", "uvicorn.workers.UvicornWorker", "--bind", "0.0.0.0:8000"]

29.2.3 容器配置管理

# container_config.py
from typing import Dict, List, Optional
from pydantic import BaseModel, Field
import os
import json

class HealthCheckConfig(BaseModel):
    """健康检查配置"""
    interval: str = "30s"
    timeout: str = "30s"
    start_period: str = "5s"
    retries: int = 3
    command: str = "curl -f http://localhost:8000/health || exit 1"

class ResourceConfig(BaseModel):
    """资源配置"""
    memory_limit: Optional[str] = None
    memory_reservation: Optional[str] = None
    cpu_limit: Optional[float] = None
    cpu_reservation: Optional[float] = None

class LoggingConfig(BaseModel):
    """日志配置"""
    driver: str = "json-file"
    options: Dict[str, str] = Field(default_factory=lambda: {
        "max-size": "10m",
        "max-file": "3"
    })

class ContainerConfig(BaseModel):
    """容器配置"""
    name: str
    image: str
    ports: Dict[str, int] = Field(default_factory=dict)
    environment: Dict[str, str] = Field(default_factory=dict)
    volumes: Dict[str, str] = Field(default_factory=dict)
    networks: List[str] = Field(default_factory=list)
    restart_policy: str = "unless-stopped"
    health_check: Optional[HealthCheckConfig] = None
    resources: Optional[ResourceConfig] = None
    logging: Optional[LoggingConfig] = None
    labels: Dict[str, str] = Field(default_factory=dict)
    depends_on: List[str] = Field(default_factory=list)
    
    @classmethod
    def from_env(cls, prefix: str = "CONTAINER_") -> "ContainerConfig":
        """从环境变量创建配置"""
        config_data = {}
        
        for key, value in os.environ.items():
            if key.startswith(prefix):
                config_key = key[len(prefix):].lower()
                
                # 处理嵌套配置
                if "_" in config_key:
                    parts = config_key.split("_")
                    current = config_data
                    for part in parts[:-1]:
                        if part not in current:
                            current[part] = {}
                        current = current[part]
                    current[parts[-1]] = value
                else:
                    config_data[config_key] = value
        
        return cls(**config_data)
    
    def to_docker_run_args(self) -> Dict:
        """转换为docker run参数"""
        args = {
            'name': self.name,
            'image': self.image,
            'ports': self.ports,
            'environment': self.environment,
            'volumes': self.volumes,
            'restart_policy': {'Name': self.restart_policy},
            'labels': self.labels,
            'detach': True
        }
        
        # 添加网络配置
        if self.networks:
            args['network'] = self.networks[0]  # Docker API限制
        
        # 添加资源限制
        if self.resources:
            host_config = {}
            if self.resources.memory_limit:
                host_config['mem_limit'] = self.resources.memory_limit
            if self.resources.memory_reservation:
                host_config['mem_reservation'] = self.resources.memory_reservation
            if self.resources.cpu_limit:
                host_config['cpu_quota'] = int(self.resources.cpu_limit * 100000)
            if self.resources.cpu_reservation:
                host_config['cpu_shares'] = int(self.resources.cpu_reservation * 1024)
            
            if host_config:
                args['host_config'] = host_config
        
        # 添加日志配置
        if self.logging:
            args['log_config'] = {
                'type': self.logging.driver,
                'config': self.logging.options
            }
        
        return args

class ContainerManager:
    """容器管理器"""
    
    def __init__(self, docker_client):
        self.client = docker_client
    
    def deploy_container(self, config: ContainerConfig) -> bool:
        """部署容器"""
        try:
            # 停止并删除现有容器
            self.stop_and_remove_container(config.name)
            
            # 获取docker run参数
            run_args = config.to_docker_run_args()
            
            # 创建并启动容器
            container = self.client.containers.run(**run_args)
            
            print(f"容器 {config.name} 部署成功: {container.id[:12]}")
            return True
            
        except Exception as e:
            print(f"容器部署失败: {e}")
            return False
    
    def stop_and_remove_container(self, name: str) -> bool:
        """停止并删除容器"""
        try:
            container = self.client.containers.get(name)
            container.stop()
            container.remove()
            print(f"已停止并删除容器: {name}")
            return True
        except Exception:
            # 容器不存在或已停止
            return True
    
    def get_container_status(self, name: str) -> Optional[Dict]:
        """获取容器状态"""
        try:
            container = self.client.containers.get(name)
            return {
                'id': container.id[:12],
                'name': container.name,
                'status': container.status,
                'image': container.image.tags[0] if container.image.tags else 'unknown',
                'ports': container.ports,
                'created': container.attrs['Created'],
                'started': container.attrs['State']['StartedAt']
            }
        except Exception:
            return None
    
    def update_container(self, config: ContainerConfig) -> bool:
        """更新容器(重新部署)"""
        return self.deploy_container(config)

29.3 多阶段构建

29.3.1 开发和生产分离

# Dockerfile.multi-stage
# 开发依赖阶段
FROM python:3.11-slim as dev-deps

WORKDIR /app

# 安装开发工具
RUN apt-get update && apt-get install -y \
    gcc \
    g++ \
    git \
    vim \
    && rm -rf /var/lib/apt/lists/*

# 复制开发依赖
COPY requirements-dev.txt .
RUN pip install --no-cache-dir -r requirements-dev.txt

# 生产依赖阶段
FROM python:3.11-slim as prod-deps

WORKDIR /app

# 安装构建依赖
RUN apt-get update && apt-get install -y \
    gcc \
    && rm -rf /var/lib/apt/lists/*

# 复制生产依赖
COPY requirements.txt .
RUN pip install --user --no-cache-dir -r requirements.txt

# 测试阶段
FROM dev-deps as test

# 复制源代码
COPY . .

# 运行测试
RUN python -m pytest tests/ -v
RUN python -m flake8 .
RUN python -m mypy .

# 构建阶段
FROM prod-deps as build

# 复制源代码
COPY . .

# 编译Python文件
RUN python -m compileall .

# 生产阶段
FROM python:3.11-slim as production

# 设置环境变量
ENV PYTHONUNBUFFERED=1 \
    PYTHONDONTWRITEBYTECODE=1 \
    PATH=/home/app/.local/bin:$PATH

# 安装运行时依赖
RUN apt-get update && apt-get install -y \
    curl \
    && rm -rf /var/lib/apt/lists/* \
    && apt-get clean

# 创建应用用户
RUN useradd --create-home --shell /bin/bash app

# 设置工作目录
WORKDIR /app

# 从构建阶段复制依赖和代码
COPY --from=build /root/.local /home/app/.local
COPY --from=build --chown=app:app /app .

# 切换到应用用户
USER app

# 健康检查
HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 \
    CMD curl -f http://localhost:8000/health || exit 1

# 暴露端口
EXPOSE 8000

# 启动命令
CMD ["gunicorn", "main:app", "-w", "4", "-k", "uvicorn.workers.UvicornWorker", "--bind", "0.0.0.0:8000"]

29.3.2 构建脚本

# build_script.py
import subprocess
import sys
import argparse
from pathlib import Path
from typing import List, Optional

class DockerBuilder:
    """Docker构建器"""
    
    def __init__(self, dockerfile_path: str = "."):
        self.dockerfile_path = Path(dockerfile_path)
        self.build_args = {}
        self.tags = []
    
    def add_build_arg(self, key: str, value: str) -> "DockerBuilder":
        """添加构建参数"""
        self.build_args[key] = value
        return self
    
    def add_tag(self, tag: str) -> "DockerBuilder":
        """添加标签"""
        self.tags.append(tag)
        return self
    
    def build(self, 
             dockerfile: str = "Dockerfile",
             target: Optional[str] = None,
             no_cache: bool = False,
             pull: bool = False) -> bool:
        """构建镜像"""
        
        cmd = ["docker", "build"]
        
        # 添加构建参数
        for key, value in self.build_args.items():
            cmd.extend(["--build-arg", f"{key}={value}"])
        
        # 添加标签
        for tag in self.tags:
            cmd.extend(["-t", tag])
        
        # 添加选项
        if target:
            cmd.extend(["--target", target])
        if no_cache:
            cmd.append("--no-cache")
        if pull:
            cmd.append("--pull")
        
        # 添加Dockerfile和上下文
        cmd.extend(["-f", dockerfile, str(self.dockerfile_path)])
        
        print(f"执行构建命令: {' '.join(cmd)}")
        
        try:
            result = subprocess.run(cmd, check=True, capture_output=True, text=True)
            print("构建成功!")
            print(result.stdout)
            return True
        except subprocess.CalledProcessError as e:
            print(f"构建失败: {e}")
            print(e.stderr)
            return False
    
    def push(self, tag: str) -> bool:
        """推送镜像"""
        cmd = ["docker", "push", tag]
        
        print(f"推送镜像: {tag}")
        
        try:
            result = subprocess.run(cmd, check=True, capture_output=True, text=True)
            print("推送成功!")
            return True
        except subprocess.CalledProcessError as e:
            print(f"推送失败: {e}")
            print(e.stderr)
            return False

def build_development():
    """构建开发环境镜像"""
    builder = DockerBuilder()
    
    success = (builder
               .add_tag("myapp:dev")
               .add_tag("myapp:latest-dev")
               .add_build_arg("BUILD_ENV", "development")
               .build(dockerfile="Dockerfile.multi-stage", target="dev-deps"))
    
    if success:
        print("开发环境镜像构建完成")
    else:
        sys.exit(1)

def build_test():
    """构建测试镜像"""
    builder = DockerBuilder()
    
    success = (builder
               .add_tag("myapp:test")
               .add_build_arg("BUILD_ENV", "test")
               .build(dockerfile="Dockerfile.multi-stage", target="test"))
    
    if success:
        print("测试镜像构建完成")
    else:
        sys.exit(1)

def build_production():
    """构建生产环境镜像"""
    builder = DockerBuilder()
    
    # 获取版本号
    try:
        result = subprocess.run(["git", "describe", "--tags", "--always"], 
                              capture_output=True, text=True, check=True)
        version = result.stdout.strip()
    except:
        version = "latest"
    
    success = (builder
               .add_tag(f"myapp:{version}")
               .add_tag("myapp:latest")
               .add_build_arg("BUILD_ENV", "production")
               .add_build_arg("VERSION", version)
               .build(dockerfile="Dockerfile.multi-stage", target="production", pull=True))
    
    if success:
        print(f"生产环境镜像构建完成: myapp:{version}")
        
        # 可选:推送到仓库
        if input("是否推送到仓库? (y/N): ").lower() == 'y':
            builder.push(f"myapp:{version}")
            builder.push("myapp:latest")
    else:
        sys.exit(1)

def main():
    parser = argparse.ArgumentParser(description="Docker构建脚本")
    parser.add_argument("target", choices=["dev", "test", "prod"], 
                       help="构建目标")
    
    args = parser.parse_args()
    
    if args.target == "dev":
        build_development()
    elif args.target == "test":
        build_test()
    elif args.target == "prod":
        build_production()

if __name__ == "__main__":
    main()

29.4 Docker Compose编排

29.4.1 基础编排配置

# docker-compose.yml
version: '3.8'

services:
  # 数据库服务
  postgres:
    image: postgres:15
    container_name: blog_postgres
    environment:
      POSTGRES_DB: ${POSTGRES_DB:-blog_db}
      POSTGRES_USER: ${POSTGRES_USER:-postgres}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-password}
    volumes:
      - postgres_data:/var/lib/postgresql/data
      - ./init-db.sql:/docker-entrypoint-initdb.d/init-db.sql
    ports:
      - "${POSTGRES_PORT:-5432}:5432"
    networks:
      - blog_network
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-postgres}"]
      interval: 10s
      timeout: 5s
      retries: 5
    restart: unless-stopped

  # Redis缓存
  redis:
    image: redis:7-alpine
    container_name: blog_redis
    command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD:-}
    volumes:
      - redis_data:/data
    ports:
      - "${REDIS_PORT:-6379}:6379"
    networks:
      - blog_network
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 5s
      retries: 5
    restart: unless-stopped

  # FastAPI应用
  app:
    build:
      context: .
      dockerfile: Dockerfile
      target: production
      args:
        BUILD_ENV: production
    container_name: blog_app
    environment:
      - DATABASE_URL=postgresql://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-password}@postgres:5432/${POSTGRES_DB:-blog_db}
      - REDIS_URL=redis://:${REDIS_PASSWORD:-}@redis:6379
      - SECRET_KEY=${SECRET_KEY:-your-secret-key}
      - DEBUG=${DEBUG:-false}
      - ALLOWED_HOSTS=${ALLOWED_HOSTS:-localhost,127.0.0.1}
    ports:
      - "${APP_PORT:-8000}:8000"
    volumes:
      - ./logs:/app/logs
      - ./uploads:/app/uploads
    networks:
      - blog_network
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s
    restart: unless-stopped
    deploy:
      resources:
        limits:
          memory: 512M
          cpus: '0.5'
        reservations:
          memory: 256M
          cpus: '0.25'

  # Nginx反向代理
  nginx:
    image: nginx:alpine
    container_name: blog_nginx
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
      - ./nginx/conf.d:/etc/nginx/conf.d:ro
      - ./static:/var/www/static:ro
      - ./uploads:/var/www/uploads:ro
    ports:
      - "${NGINX_PORT:-80}:80"
      - "${NGINX_SSL_PORT:-443}:443"
    networks:
      - blog_network
    depends_on:
      - app
    healthcheck:
      test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost/health"]
      interval: 30s
      timeout: 10s
      retries: 3
    restart: unless-stopped

volumes:
  postgres_data:
    driver: local
  redis_data:
    driver: local

networks:
  blog_network:
    driver: bridge
    ipam:
      config:
        - subnet: 172.20.0.0/16

29.4.2 环境配置文件

# .env
# 数据库配置
POSTGRES_DB=blog_db
POSTGRES_USER=postgres
POSTGRES_PASSWORD=your_secure_password
POSTGRES_PORT=5432

# Redis配置
REDIS_PASSWORD=your_redis_password
REDIS_PORT=6379

# 应用配置
SECRET_KEY=your-very-secure-secret-key
DEBUG=false
ALLOWED_HOSTS=localhost,127.0.0.1,yourdomain.com
APP_PORT=8000

# Nginx配置
NGINX_PORT=80
NGINX_SSL_PORT=443

# 日志级别
LOG_LEVEL=INFO

# 邮件配置
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_USER=your-email@gmail.com
SMTP_PASSWORD=your-email-password
# .env.dev
# 开发环境配置
POSTGRES_DB=blog_dev_db
POSTGRES_USER=postgres
POSTGRES_PASSWORD=password
POSTGRES_PORT=5432

REDIS_PASSWORD=
REDIS_PORT=6379

SECRET_KEY=dev-secret-key
DEBUG=true
ALLOWED_HOSTS=*
APP_PORT=8000

NGINX_PORT=80
NGINX_SSL_PORT=443

LOG_LEVEL=DEBUG

29.4.3 开发环境编排

# docker-compose.dev.yml
version: '3.8'

services:
  postgres:
    ports:
      - "5432:5432"
    environment:
      POSTGRES_DB: blog_dev_db
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: password

  redis:
    ports:
      - "6379:6379"
    command: redis-server --appendonly yes

  app:
    build:
      target: dev-deps
      args:
        BUILD_ENV: development
    environment:
      - DATABASE_URL=postgresql://postgres:password@postgres:5432/blog_dev_db
      - REDIS_URL=redis://redis:6379
      - SECRET_KEY=dev-secret-key
      - DEBUG=true
      - RELOAD=true
    volumes:
      - .:/app
      - /app/__pycache__
    command: uvicorn main:app --host 0.0.0.0 --port 8000 --reload
    ports:
      - "8000:8000"

  # 开发工具
  adminer:
    image: adminer
    container_name: blog_adminer
    ports:
      - "8080:8080"
    networks:
      - blog_network
    depends_on:
      - postgres

  # Redis管理界面
  redis-commander:
    image: rediscommander/redis-commander:latest
    container_name: blog_redis_commander
    environment:
      - REDIS_HOSTS=local:redis:6379
    ports:
      - "8081:8081"
    networks:
      - blog_network
    depends_on:
      - redis

29.4.4 生产环境编排

# docker-compose.prod.yml
version: '3.8'

services:
  postgres:
    deploy:
      resources:
        limits:
          memory: 1G
          cpus: '1.0'
        reservations:
          memory: 512M
          cpus: '0.5'
    volumes:
      - postgres_data:/var/lib/postgresql/data
      - ./backups:/backups
    environment:
      POSTGRES_DB: ${POSTGRES_DB}
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}

  redis:
    deploy:
      resources:
        limits:
          memory: 256M
          cpus: '0.5'
        reservations:
          memory: 128M
          cpus: '0.25'
    command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD}

  app:
    build:
      target: production
      args:
        BUILD_ENV: production
    deploy:
      replicas: 2
      resources:
        limits:
          memory: 512M
          cpus: '0.5'
        reservations:
          memory: 256M
          cpus: '0.25'
      restart_policy:
        condition: on-failure
        delay: 5s
        max_attempts: 3
    environment:
      - DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB}
      - REDIS_URL=redis://:${REDIS_PASSWORD}@redis:6379
      - SECRET_KEY=${SECRET_KEY}
      - DEBUG=false
      - WORKERS=4
    volumes:
      - ./logs:/app/logs
      - ./uploads:/app/uploads
    command: gunicorn main:app -w 4 -k uvicorn.workers.UvicornWorker --bind 0.0.0.0:8000

  nginx:
    volumes:
      - ./nginx/nginx.prod.conf:/etc/nginx/nginx.conf:ro
      - ./ssl:/etc/nginx/ssl:ro
    deploy:
      resources:
        limits:
          memory: 128M
          cpus: '0.25'
        reservations:
          memory: 64M
          cpus: '0.1'

  # 监控服务
  prometheus:
    image: prom/prometheus:latest
    container_name: blog_prometheus
    volumes:
      - ./monitoring/prometheus.yml:/etc/prometheus/prometheus.yml:ro
      - prometheus_data:/prometheus
    ports:
      - "9090:9090"
    networks:
      - blog_network
    command:
      - '--config.file=/etc/prometheus/prometheus.yml'
      - '--storage.tsdb.path=/prometheus'
      - '--web.console.libraries=/etc/prometheus/console_libraries'
      - '--web.console.templates=/etc/prometheus/consoles'
      - '--web.enable-lifecycle'

  grafana:
    image: grafana/grafana:latest
    container_name: blog_grafana
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_PASSWORD:-admin}
    volumes:
      - grafana_data:/var/lib/grafana
      - ./monitoring/grafana:/etc/grafana/provisioning
    ports:
      - "3000:3000"
    networks:
      - blog_network
    depends_on:
      - prometheus

volumes:
  prometheus_data:
  grafana_data:

29.5 容器编排管理

29.5.1 编排管理脚本

# compose_manager.py
import subprocess
import sys
import os
import yaml
from typing import List, Optional, Dict
from pathlib import Path

class ComposeManager:
    """Docker Compose管理器"""
    
    def __init__(self, project_name: str = None, env_file: str = None):
        self.project_name = project_name
        self.env_file = env_file
        self.base_cmd = ["docker-compose"]
        
        if project_name:
            self.base_cmd.extend(["-p", project_name])
        if env_file:
            self.base_cmd.extend(["--env-file", env_file])
    
    def _run_command(self, cmd: List[str], check: bool = True) -> subprocess.CompletedProcess:
        """执行命令"""
        full_cmd = self.base_cmd + cmd
        print(f"执行命令: {' '.join(full_cmd)}")
        
        return subprocess.run(full_cmd, check=check, capture_output=True, text=True)
    
    def up(self, 
           services: Optional[List[str]] = None,
           detach: bool = True,
           build: bool = False,
           force_recreate: bool = False) -> bool:
        """启动服务"""
        cmd = ["up"]
        
        if detach:
            cmd.append("-d")
        if build:
            cmd.append("--build")
        if force_recreate:
            cmd.append("--force-recreate")
        
        if services:
            cmd.extend(services)
        
        try:
            result = self._run_command(cmd)
            print("服务启动成功")
            return True
        except subprocess.CalledProcessError as e:
            print(f"服务启动失败: {e}")
            print(e.stderr)
            return False
    
    def down(self, 
             remove_volumes: bool = False,
             remove_images: str = None) -> bool:
        """停止服务"""
        cmd = ["down"]
        
        if remove_volumes:
            cmd.append("-v")
        if remove_images:
            cmd.extend(["--rmi", remove_images])
        
        try:
            result = self._run_command(cmd)
            print("服务停止成功")
            return True
        except subprocess.CalledProcessError as e:
            print(f"服务停止失败: {e}")
            print(e.stderr)
            return False
    
    def restart(self, services: Optional[List[str]] = None) -> bool:
        """重启服务"""
        cmd = ["restart"]
        
        if services:
            cmd.extend(services)
        
        try:
            result = self._run_command(cmd)
            print("服务重启成功")
            return True
        except subprocess.CalledProcessError as e:
            print(f"服务重启失败: {e}")
            print(e.stderr)
            return False
    
    def build(self, 
             services: Optional[List[str]] = None,
             no_cache: bool = False,
             pull: bool = False) -> bool:
        """构建服务"""
        cmd = ["build"]
        
        if no_cache:
            cmd.append("--no-cache")
        if pull:
            cmd.append("--pull")
        
        if services:
            cmd.extend(services)
        
        try:
            result = self._run_command(cmd)
            print("服务构建成功")
            return True
        except subprocess.CalledProcessError as e:
            print(f"服务构建失败: {e}")
            print(e.stderr)
            return False
    
    def logs(self, 
            services: Optional[List[str]] = None,
            follow: bool = False,
            tail: Optional[int] = None) -> bool:
        """查看日志"""
        cmd = ["logs"]
        
        if follow:
            cmd.append("-f")
        if tail:
            cmd.extend(["--tail", str(tail)])
        
        if services:
            cmd.extend(services)
        
        try:
            result = self._run_command(cmd, check=False)
            print(result.stdout)
            return True
        except Exception as e:
            print(f"查看日志失败: {e}")
            return False
    
    def ps(self) -> bool:
        """查看服务状态"""
        try:
            result = self._run_command(["ps"])
            print(result.stdout)
            return True
        except subprocess.CalledProcessError as e:
            print(f"查看状态失败: {e}")
            return False
    
    def exec(self, service: str, command: List[str], interactive: bool = True) -> bool:
        """在服务中执行命令"""
        cmd = ["exec"]
        
        if interactive:
            cmd.append("-it")
        
        cmd.append(service)
        cmd.extend(command)
        
        try:
            # 对于交互式命令,不捕获输出
            if interactive:
                subprocess.run(self.base_cmd + cmd, check=True)
            else:
                result = self._run_command(cmd)
                print(result.stdout)
            return True
        except subprocess.CalledProcessError as e:
            print(f"执行命令失败: {e}")
            return False
    
    def scale(self, service_replicas: Dict[str, int]) -> bool:
        """扩缩容服务"""
        cmd = ["up", "-d", "--scale"]
        
        for service, replicas in service_replicas.items():
            cmd.append(f"{service}={replicas}")
        
        try:
            result = self._run_command(cmd)
            print("服务扩缩容成功")
            return True
        except subprocess.CalledProcessError as e:
            print(f"服务扩缩容失败: {e}")
            print(e.stderr)
            return False
    
    def config(self) -> Optional[Dict]:
        """验证并查看配置"""
        try:
            result = self._run_command(["config"])
            config = yaml.safe_load(result.stdout)
            return config
        except subprocess.CalledProcessError as e:
            print(f"配置验证失败: {e}")
            print(e.stderr)
            return None
        except yaml.YAMLError as e:
            print(f"配置解析失败: {e}")
            return None

class DeploymentManager:
    """部署管理器"""
    
    def __init__(self):
        self.environments = {
            'dev': {
                'compose_files': ['docker-compose.yml', 'docker-compose.dev.yml'],
                'env_file': '.env.dev'
            },
            'staging': {
                'compose_files': ['docker-compose.yml', 'docker-compose.staging.yml'],
                'env_file': '.env.staging'
            },
            'prod': {
                'compose_files': ['docker-compose.yml', 'docker-compose.prod.yml'],
                'env_file': '.env'
            }
        }
    
    def deploy(self, environment: str, services: Optional[List[str]] = None) -> bool:
        """部署到指定环境"""
        if environment not in self.environments:
            print(f"未知环境: {environment}")
            return False
        
        env_config = self.environments[environment]
        
        # 设置compose文件
        compose_files = env_config['compose_files']
        env_file = env_config['env_file']
        
        # 检查文件是否存在
        for file in compose_files + [env_file]:
            if not Path(file).exists():
                print(f"文件不存在: {file}")
                return False
        
        # 创建管理器
        manager = ComposeManager(
            project_name=f"blog_{environment}",
            env_file=env_file
        )
        
        # 添加compose文件参数
        for file in compose_files:
            manager.base_cmd.extend(["-f", file])
        
        print(f"开始部署到 {environment} 环境...")
        
        # 构建镜像
        if not manager.build(services, no_cache=environment == 'prod'):
            return False
        
        # 启动服务
        if not manager.up(services, build=False, force_recreate=environment == 'prod'):
            return False
        
        print(f"部署到 {environment} 环境成功")
        return True
    
    def rollback(self, environment: str, services: Optional[List[str]] = None) -> bool:
        """回滚部署"""
        if environment not in self.environments:
            print(f"未知环境: {environment}")
            return False
        
        env_config = self.environments[environment]
        
        manager = ComposeManager(
            project_name=f"blog_{environment}",
            env_file=env_config['env_file']
        )
        
        for file in env_config['compose_files']:
            manager.base_cmd.extend(["-f", file])
        
        print(f"开始回滚 {environment} 环境...")
        
        # 停止当前服务
        if not manager.down():
            return False
        
        # 重新启动服务(使用之前的镜像)
        if not manager.up(services):
            return False
        
        print(f"回滚 {environment} 环境成功")
        return True

29.5.2 部署脚本

#!/bin/bash
# deploy.sh

set -e

# 颜色定义
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color

# 日志函数
log_info() {
    echo -e "${GREEN}[INFO]${NC} $1"
}

log_warn() {
    echo -e "${YELLOW}[WARN]${NC} $1"
}

log_error() {
    echo -e "${RED}[ERROR]${NC} $1"
}

# 检查依赖
check_dependencies() {
    log_info "检查依赖..."
    
    if ! command -v docker &> /dev/null; then
        log_error "Docker未安装"
        exit 1
    fi
    
    if ! command -v docker-compose &> /dev/null; then
        log_error "Docker Compose未安装"
        exit 1
    fi
    
    log_info "依赖检查通过"
}

# 检查环境文件
check_env_file() {
    local env_file=$1
    
    if [[ ! -f "$env_file" ]]; then
        log_error "环境文件不存在: $env_file"
        exit 1
    fi
    
    log_info "环境文件检查通过: $env_file"
}

# 备份数据
backup_data() {
    local environment=$1
    local backup_dir="backups/$(date +%Y%m%d_%H%M%S)"
    
    log_info "创建数据备份..."
    
    mkdir -p "$backup_dir"
    
    # 备份数据库
    docker-compose -p "blog_${environment}" exec -T postgres pg_dump -U postgres blog_db > "$backup_dir/database.sql"
    
    # 备份上传文件
    if [[ -d "uploads" ]]; then
        cp -r uploads "$backup_dir/"
    fi
    
    log_info "数据备份完成: $backup_dir"
}

# 部署到开发环境
deploy_dev() {
    log_info "部署到开发环境..."
    
    check_env_file ".env.dev"
    
    docker-compose -f docker-compose.yml -f docker-compose.dev.yml --env-file .env.dev -p blog_dev up -d --build
    
    log_info "开发环境部署完成"
    log_info "应用地址: http://localhost:8000"
    log_info "数据库管理: http://localhost:8080"
    log_info "Redis管理: http://localhost:8081"
}

# 部署到生产环境
deploy_prod() {
    log_info "部署到生产环境..."
    
    check_env_file ".env"
    
    # 创建备份
    backup_data "prod"
    
    # 构建生产镜像
    docker-compose -f docker-compose.yml -f docker-compose.prod.yml build --no-cache
    
    # 启动服务
    docker-compose -f docker-compose.yml -f docker-compose.prod.yml --env-file .env -p blog_prod up -d
    
    # 等待服务启动
    log_info "等待服务启动..."
    sleep 30
    
    # 健康检查
    if curl -f http://localhost/health > /dev/null 2>&1; then
        log_info "生产环境部署成功"
        log_info "应用地址: http://localhost"
    else
        log_error "健康检查失败,请检查服务状态"
        exit 1
    fi
}

# 停止服务
stop_services() {
    local environment=$1
    
    log_info "停止 $environment 环境服务..."
    
    case $environment in
        "dev")
            docker-compose -f docker-compose.yml -f docker-compose.dev.yml -p blog_dev down
            ;;
        "prod")
            docker-compose -f docker-compose.yml -f docker-compose.prod.yml -p blog_prod down
            ;;
        *)
            log_error "未知环境: $environment"
            exit 1
            ;;
    esac
    
    log_info "$environment 环境服务已停止"
}

# 查看日志
view_logs() {
    local environment=$1
    local service=$2
    
    case $environment in
        "dev")
            docker-compose -f docker-compose.yml -f docker-compose.dev.yml -p blog_dev logs -f $service
            ;;
        "prod")
            docker-compose -f docker-compose.yml -f docker-compose.prod.yml -p blog_prod logs -f $service
            ;;
        *)
            log_error "未知环境: $environment"
            exit 1
            ;;
    esac
}

# 显示帮助
show_help() {
    echo "用法: $0 [命令] [选项]"
    echo ""
    echo "命令:"
    echo "  dev                 部署到开发环境"
    echo "  prod                部署到生产环境"
    echo "  stop <env>          停止指定环境的服务"
    echo "  logs <env> [service] 查看指定环境的日志"
    echo "  backup <env>        备份指定环境的数据"
    echo "  help                显示此帮助信息"
    echo ""
    echo "示例:"
    echo "  $0 dev              # 部署开发环境"
    echo "  $0 prod             # 部署生产环境"
    echo "  $0 stop dev         # 停止开发环境"
    echo "  $0 logs prod app    # 查看生产环境应用日志"
}

# 主函数
main() {
    check_dependencies
    
    case "${1:-help}" in
        "dev")
            deploy_dev
            ;;
        "prod")
            deploy_prod
            ;;
        "stop")
            if [[ -z "$2" ]]; then
                log_error "请指定环境"
                show_help
                exit 1
            fi
            stop_services "$2"
            ;;
        "logs")
            if [[ -z "$2" ]]; then
                log_error "请指定环境"
                show_help
                exit 1
            fi
            view_logs "$2" "$3"
            ;;
        "backup")
            if [[ -z "$2" ]]; then
                log_error "请指定环境"
                show_help
                exit 1
            fi
            backup_data "$2"
            ;;
        "help")
            show_help
            ;;
        *)
            log_error "未知命令: $1"
            show_help
            exit 1
            ;;
    esac
}

main "$@"

29.6 生产环境优化

29.6.1 性能优化配置

# production_config.py
from typing import Dict, Any
import os

class ProductionConfig:
    """生产环境配置"""
    
    # 应用配置
    APP_CONFIG = {
        'workers': int(os.getenv('WORKERS', '4')),
        'worker_class': 'uvicorn.workers.UvicornWorker',
        'worker_connections': int(os.getenv('WORKER_CONNECTIONS', '1000')),
        'max_requests': int(os.getenv('MAX_REQUESTS', '1000')),
        'max_requests_jitter': int(os.getenv('MAX_REQUESTS_JITTER', '100')),
        'timeout': int(os.getenv('TIMEOUT', '30')),
        'keepalive': int(os.getenv('KEEPALIVE', '2')),
        'bind': f"0.0.0.0:{os.getenv('PORT', '8000')}",
        'preload_app': True,
        'access_log': True,
        'error_log': '-',
        'log_level': os.getenv('LOG_LEVEL', 'info').lower()
    }
    
    # 数据库连接池配置
    DATABASE_CONFIG = {
        'pool_size': int(os.getenv('DB_POOL_SIZE', '20')),
        'max_overflow': int(os.getenv('DB_MAX_OVERFLOW', '30')),
        'pool_timeout': int(os.getenv('DB_POOL_TIMEOUT', '30')),
        'pool_recycle': int(os.getenv('DB_POOL_RECYCLE', '3600')),
        'pool_pre_ping': True
    }
    
    # Redis配置
    REDIS_CONFIG = {
        'max_connections': int(os.getenv('REDIS_MAX_CONNECTIONS', '50')),
        'retry_on_timeout': True,
        'socket_timeout': int(os.getenv('REDIS_SOCKET_TIMEOUT', '5')),
        'socket_connect_timeout': int(os.getenv('REDIS_CONNECT_TIMEOUT', '5'))
    }
    
    # 缓存配置
    CACHE_CONFIG = {
        'default_timeout': int(os.getenv('CACHE_DEFAULT_TIMEOUT', '300')),
        'key_prefix': os.getenv('CACHE_KEY_PREFIX', 'blog:'),
        'version': int(os.getenv('CACHE_VERSION', '1'))
    }
    
    @classmethod
    def get_gunicorn_config(cls) -> Dict[str, Any]:
        """获取Gunicorn配置"""
        return cls.APP_CONFIG
    
    @classmethod
    def get_database_url(cls) -> str:
        """获取数据库URL"""
        base_url = os.getenv('DATABASE_URL')
        if not base_url:
            raise ValueError("DATABASE_URL环境变量未设置")
        
        # 添加连接池参数
        pool_params = [
            f"pool_size={cls.DATABASE_CONFIG['pool_size']}",
            f"max_overflow={cls.DATABASE_CONFIG['max_overflow']}",
            f"pool_timeout={cls.DATABASE_CONFIG['pool_timeout']}",
            f"pool_recycle={cls.DATABASE_CONFIG['pool_recycle']}"
        ]
        
        separator = '&' if '?' in base_url else '?'
        return f"{base_url}{separator}{'&'.join(pool_params)}"

29.6.2 Gunicorn配置文件

# gunicorn.conf.py
import multiprocessing
import os

# 服务器套接字
bind = f"0.0.0.0:{os.getenv('PORT', '8000')}"
backlog = 2048

# 工作进程
workers = int(os.getenv('WORKERS', multiprocessing.cpu_count() * 2 + 1))
worker_class = "uvicorn.workers.UvicornWorker"
worker_connections = 1000
max_requests = 1000
max_requests_jitter = 100

# 超时设置
timeout = 30
keepalive = 2
graceful_timeout = 30

# 预加载应用
preload_app = True

# 日志配置
accesslog = "-"
errorlog = "-"
loglevel = os.getenv('LOG_LEVEL', 'info').lower()
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 = 'fastapi-blog'

# 安全设置
limit_request_line = 4094
limit_request_fields = 100
limit_request_field_size = 8190

# 性能调优
worker_tmp_dir = '/dev/shm'

# 钩子函数
def on_starting(server):
    """服务器启动时调用"""
    server.log.info("FastAPI Blog服务器启动中...")

def on_reload(server):
    """配置重载时调用"""
    server.log.info("配置已重载")

def when_ready(server):
    """服务器准备就绪时调用"""
    server.log.info("FastAPI Blog服务器已就绪")

def worker_int(worker):
    """工作进程收到SIGINT信号时调用"""
    worker.log.info(f"工作进程 {worker.pid} 收到中断信号")

def pre_fork(server, worker):
    """工作进程fork前调用"""
    server.log.info(f"工作进程 {worker.pid} 即将启动")

def post_fork(server, worker):
    """工作进程fork后调用"""
    server.log.info(f"工作进程 {worker.pid} 已启动")

def worker_abort(worker):
    """工作进程异常终止时调用"""
    worker.log.error(f"工作进程 {worker.pid} 异常终止")

29.6.3 Nginx配置

# nginx/nginx.conf
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;

events {
    worker_connections 1024;
    use epoll;
    multi_accept on;
}

http {
    include /etc/nginx/mime.types;
    default_type application/octet-stream;
    
    # 日志格式
    log_format main '$remote_addr - $remote_user [$time_local] "$request" '
                    '$status $body_bytes_sent "$http_referer" '
                    '"$http_user_agent" "$http_x_forwarded_for" '
                    '$request_time $upstream_response_time';
    
    access_log /var/log/nginx/access.log main;
    
    # 基础设置
    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;
    keepalive_timeout 65;
    types_hash_max_size 2048;
    client_max_body_size 100M;
    
    # Gzip压缩
    gzip on;
    gzip_vary on;
    gzip_min_length 1024;
    gzip_proxied any;
    gzip_comp_level 6;
    gzip_types
        text/plain
        text/css
        text/xml
        text/javascript
        application/json
        application/javascript
        application/xml+rss
        application/atom+xml
        image/svg+xml;
    
    # 上游服务器
    upstream fastapi_backend {
        least_conn;
        server app:8000 max_fails=3 fail_timeout=30s;
        keepalive 32;
    }
    
    # 限流配置
    limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
    limit_req_zone $binary_remote_addr zone=login:10m rate=1r/s;
    
    # 主服务器配置
    server {
        listen 80;
        server_name localhost;
        
        # 安全头
        add_header X-Frame-Options "SAMEORIGIN" always;
        add_header X-Content-Type-Options "nosniff" always;
        add_header X-XSS-Protection "1; mode=block" always;
        add_header Referrer-Policy "strict-origin-when-cross-origin" always;
        
        # 静态文件
        location /static/ {
            alias /var/www/static/;
            expires 1y;
            add_header Cache-Control "public, immutable";
            access_log off;
        }
        
        # 上传文件
        location /uploads/ {
            alias /var/www/uploads/;
            expires 1M;
            add_header Cache-Control "public";
        }
        
        # 健康检查
        location /health {
            proxy_pass http://fastapi_backend;
            access_log off;
        }
        
        # API限流
        location /api/ {
            limit_req zone=api burst=20 nodelay;
            proxy_pass http://fastapi_backend;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
            proxy_connect_timeout 30s;
            proxy_send_timeout 30s;
            proxy_read_timeout 30s;
        }
        
        # 登录限流
        location /api/auth/login {
            limit_req zone=login burst=5 nodelay;
            proxy_pass http://fastapi_backend;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }
        
        # 默认代理
        location / {
            proxy_pass http://fastapi_backend;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
            proxy_connect_timeout 30s;
            proxy_send_timeout 30s;
            proxy_read_timeout 30s;
            
            # 缓存设置
            proxy_cache_bypass $http_upgrade;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection 'upgrade';
        }
     }
 }

29.6.4 监控配置

# monitoring/docker-compose.monitoring.yml
version: '3.8'

services:
  prometheus:
    image: prom/prometheus:latest
    container_name: prometheus
    ports:
      - "9090:9090"
    volumes:
      - ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
      - prometheus_data:/prometheus
    command:
      - '--config.file=/etc/prometheus/prometheus.yml'
      - '--storage.tsdb.path=/prometheus'
      - '--web.console.libraries=/etc/prometheus/console_libraries'
      - '--web.console.templates=/etc/prometheus/consoles'
      - '--storage.tsdb.retention.time=200h'
      - '--web.enable-lifecycle'
    networks:
      - monitoring

  grafana:
    image: grafana/grafana:latest
    container_name: grafana
    ports:
      - "3000:3000"
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=admin123
    volumes:
      - grafana_data:/var/lib/grafana
      - ./grafana/provisioning:/etc/grafana/provisioning
    networks:
      - monitoring

  node-exporter:
    image: prom/node-exporter:latest
    container_name: node-exporter
    ports:
      - "9100:9100"
    volumes:
      - /proc:/host/proc:ro
      - /sys:/host/sys:ro
      - /:/rootfs:ro
    command:
      - '--path.procfs=/host/proc'
      - '--path.rootfs=/rootfs'
      - '--path.sysfs=/host/sys'
      - '--collector.filesystem.mount-points-exclude=^/(sys|proc|dev|host|etc)($$|/)'
    networks:
      - monitoring

  cadvisor:
    image: gcr.io/cadvisor/cadvisor:latest
    container_name: cadvisor
    ports:
      - "8080:8080"
    volumes:
      - /:/rootfs:ro
      - /var/run:/var/run:rw
      - /sys:/sys:ro
      - /var/lib/docker/:/var/lib/docker:ro
    networks:
      - monitoring

volumes:
  prometheus_data:
  grafana_data:

networks:
  monitoring:
    driver: bridge
# prometheus/prometheus.yml
global:
  scrape_interval: 15s
  evaluation_interval: 15s

rule_files:
  - "rules/*.yml"

scrape_configs:
  - job_name: 'prometheus'
    static_configs:
      - targets: ['localhost:9090']

  - job_name: 'fastapi-blog'
    static_configs:
      - targets: ['app:8000']
    metrics_path: '/metrics'
    scrape_interval: 5s

  - job_name: 'node-exporter'
    static_configs:
      - targets: ['node-exporter:9100']

  - job_name: 'cadvisor'
    static_configs:
      - targets: ['cadvisor:8080']

  - job_name: 'postgres'
    static_configs:
      - targets: ['postgres:5432']

  - job_name: 'redis'
    static_configs:
      - targets: ['redis:6379']

29.7 实践练习

练习1:多环境部署

  1. 创建测试环境的Docker配置
  2. 实现自动化的环境切换脚本
  3. 配置不同环境的资源限制

练习2:容器优化

  1. 优化Docker镜像大小
  2. 实现多阶段构建优化
  3. 配置容器健康检查

练习3:监控集成

  1. 集成Prometheus监控
  2. 配置Grafana仪表板
  3. 设置告警规则

练习4:安全加固

  1. 实现容器安全扫描
  2. 配置网络安全策略
  3. 实现密钥管理

29.8 本章小结

本章详细介绍了FastAPI应用的容器化部署:

  1. Docker基础:学习了Docker环境配置和Dockerfile编写
  2. 单容器部署:掌握了基础和优化的容器配置
  3. 多阶段构建:实现了开发和生产环境的分离
  4. Docker Compose:学习了多服务编排和环境管理
  5. 容器编排:实现了完整的部署和管理脚本
  6. 生产优化:配置了性能优化和监控系统

通过本章学习,你应该能够:

  • 编写高效的Dockerfile
  • 使用Docker Compose管理多服务应用
  • 实现多环境的容器化部署
  • 配置生产环境的性能优化
  • 集成监控和日志系统

下一章我们将学习云平台部署,包括AWS、Azure和Google Cloud的部署策略。

Logo

码道开发者社区,聚焦华为云码道 CodeArts 代码智能体,沉淀 Agent、Skill、鸿蒙开发实战内容,供开发者查阅资料、交流技术、分享工程实践

更多推荐