Python全栈实战:Docker Compose + Nginx 部署 FastAPI、Vue3 与 MySQL

完成一个全栈项目的开发只是第一步,将它真正部署到生产环境并稳定运行才是更大的挑战。本文将带你从零开始,使用Docker Compose和Nginx,将FastAPI后端、Vue3前端和MySQL数据库部署为一个完整的生产级应用。这不仅是一个教程,更是一套可复用的部署模板。


一、最终架构概览

浏览器
    │
    │ 80端口
    ▼
Nginx(反向代理 + 静态资源服务)
    │
    ├── /api/* ──────► FastAPI:8000(后端API)
    ├── /static/* ───► Nginx直接返回静态文件
    └── /* ──────────► Vue3前端静态页面
                        │
                        ▼
                    MySQL:3306(数据库)
                        │
                        ▼
                    Docker Volume(数据持久化)

这套架构的核心设计思想:

  • Nginx作为统一入口,处理静态资源、反向代理和负载均衡
  • FastAPI只负责业务逻辑和API,不处理静态文件
  • Vue3编译为纯静态文件,由Nginx直接服务
  • MySQL通过Docker Volume持久化数据,确保容器重启不丢失

二、项目结构

project/
├── backend/
│   ├── app/
│   │   ├── __init__.py
│   │   ├── main.py          # FastAPI入口
│   │   ├── models.py        # SQLAlchemy模型
│   │   ├── schemas.py       # Pydantic模型
│   │   ├── crud.py          # 数据库操作
│   │   ├── database.py      # 数据库连接
│   │   └── routers/         # API路由
│   │       ├── __init__.py
│   │       ├── auth.py      # 认证路由
│   │       └── users.py     # 用户路由
│   ├── requirements.txt
│   ├── Dockerfile
│   └── alembic/             # 数据库迁移
├── frontend/
│   ├── src/
│   ├── package.json
│   ├── vite.config.ts
│   ├── Dockerfile
│   └── nginx.conf           # 前端Nginx配置
├── nginx/
│   └── nginx.conf           # 主Nginx配置
├── docker-compose.yml
└── .env

三、FastAPI后端实现

3.1 数据库连接

# backend/app/database.py
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
import os

DATABASE_URL = os.getenv(
    "DATABASE_URL",
    "mysql+pymysql://user:password@db:3306/app_db"
)

engine = create_engine(
    DATABASE_URL,
    pool_size=20,
    max_overflow=40,
    pool_pre_ping=True,       # 自动检测连接是否有效
    pool_recycle=3600,        # 每小时回收连接
)

SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()

def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()

3.2 用户模型

# backend/app/models.py
from sqlalchemy import Column, Integer, String, Boolean, DateTime
from sqlalchemy.sql import func
from .database import Base

class User(Base):
    __tablename__ = "users"
    
    id = Column(Integer, primary_key=True, index=True)
    username = Column(String(50), unique=True, index=True, nullable=False)
    email = Column(String(100), unique=True, index=True, nullable=False)
    hashed_password = Column(String(255), nullable=False)
    is_active = Column(Boolean, default=True)
    is_superuser = Column(Boolean, default=False)
    created_at = Column(DateTime(timezone=True), server_default=func.now())
    updated_at = Column(DateTime(timezone=True), onupdate=func.now())

3.3 JWT认证

# backend/app/routers/auth.py
from datetime import datetime, timedelta
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from jose import JWTError, jwt
from passlib.context import CryptContext
from pydantic import BaseModel

router = APIRouter(prefix="/auth", tags=["认证"])

# 配置
SECRET_KEY = os.getenv("SECRET_KEY", "your-secret-key-change-in-production")
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30

pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/auth/login")

class Token(BaseModel):
    access_token: str
    token_type: str

class TokenData(BaseModel):
    username: str | None = None

def create_access_token(data: dict, expires_delta: timedelta | None = None):
    to_encode = data.copy()
    expire = datetime.utcnow() + (expires_delta or timedelta(minutes=15))
    to_encode.update({"exp": expire})
    return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)

@router.post("/login", response_model=Token)
async def login(form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db)):
    user = db.query(User).filter(User.username == form_data.username).first()
    if not user or not pwd_context.verify(form_data.password, user.hashed_password):
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="用户名或密码错误",
            headers={"WWW-Authenticate": "Bearer"},
        )
    access_token = create_access_token(
        data={"sub": user.username},
        expires_delta=timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
    )
    return {"access_token": access_token, "token_type": "bearer"}

async def get_current_user(
    token: str = Depends(oauth2_scheme),
    db: Session = Depends(get_db)
):
    credentials_exception = HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED,
        detail="无法验证凭据",
        headers={"WWW-Authenticate": "Bearer"},
    )
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
        username: str = payload.get("sub")
        if username is None:
            raise credentials_exception
    except JWTError:
        raise credentials_exception
    
    user = db.query(User).filter(User.username == username).first()
    if user is None:
        raise credentials_exception
    return user

3.4 FastAPI应用入口

# backend/app/main.py
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from .routers import auth, users

app = FastAPI(
    title="全栈应用API",
    version="1.0.0",
    docs_url="/api/docs",
    redoc_url="/api/redoc",
)

# CORS配置
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],  # 生产环境应限制具体域名
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

# 注册路由
app.include_router(auth.router, prefix="/api")
app.include_router(users.router, prefix="/api")

@app.get("/api/health")
async def health_check():
    return {"status": "healthy", "version": "1.0.0"}

3.5 后端Dockerfile

# backend/Dockerfile
FROM python:3.12-slim

WORKDIR /app

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

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

# 复制应用代码
COPY . .

# 创建非root用户
RUN useradd -m -u 1000 appuser && chown -R appuser:appuser /app
USER appuser

# 使用uvicorn运行
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "4"]

四、Vue3前端

4.1 Vite配置

// frontend/vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'

export default defineConfig({
  plugins: [vue()],
  resolve: {
    alias: {
      '@': resolve(__dirname, 'src'),
    },
  },
  server: {
    port: 5173,
    proxy: {
      '/api': {
        target: 'http://localhost:8000',
        changeOrigin: true,
      },
    },
  },
  build: {
    outDir: 'dist',
    assetsDir: 'assets',
    sourcemap: false,
    rollupOptions: {
      output: {
        manualChunks: {
          'vue-vendor': ['vue', 'vue-router', 'pinia'],
          'ui-vendor': ['element-plus'],
        },
      },
    },
  },
})

4.2 前端Dockerfile(多阶段构建)

# frontend/Dockerfile
# 阶段1:构建
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build

# 阶段2:生产运行
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

4.3 前端Nginx配置

# frontend/nginx.conf
server {
    listen 80;
    server_name localhost;
    root /usr/share/nginx/html;
    index index.html;

    # Vue Router history模式
    location / {
        try_files $uri $uri/ /index.html;
    }

    # 静态资源缓存
    location /assets {
        expires 1y;
        add_header Cache-Control "public, immutable";
    }

    # API代理
    location /api/ {
        proxy_pass http://backend:8000/api/;
        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;
    }
}

五、Docker Compose编排

# docker-compose.yml
version: '3.8'

services:
  # MySQL数据库
  db:
    image: mysql:8.0
    container_name: app_db
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
      MYSQL_DATABASE: ${MYSQL_DATABASE}
      MYSQL_USER: ${MYSQL_USER}
      MYSQL_PASSWORD: ${MYSQL_PASSWORD}
    ports:
      - "3306:3306"
    volumes:
      - mysql_data:/var/lib/mysql
      - ./db/init.sql:/docker-entrypoint-initdb.d/init.sql
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 10s
      timeout: 5s
      retries: 5
    networks:
      - app_network

  # FastAPI后端
  backend:
    build:
      context: ./backend
      dockerfile: Dockerfile
    container_name: app_backend
    restart: always
    environment:
      DATABASE_URL: mysql+pymysql://${MYSQL_USER}:${MYSQL_PASSWORD}@db:3306/${MYSQL_DATABASE}
      SECRET_KEY: ${SECRET_KEY}
    depends_on:
      db:
        condition: service_healthy
    networks:
      - app_network

  # Vue3前端 + Nginx
  frontend:
    build:
      context: ./frontend
      dockerfile: Dockerfile
    container_name: app_frontend
    restart: always
    ports:
      - "80:80"
    depends_on:
      - backend
    networks:
      - app_network

volumes:
  mysql_data:
    driver: local

networks:
  app_network:
    driver: bridge

六、部署与运维

6.1 启动服务

# 构建并启动所有服务
docker-compose up -d --build

# 查看运行状态
docker-compose ps

# 查看日志
docker-compose logs -f backend

# 执行数据库迁移
docker-compose exec backend alembic upgrade head

6.2 健康检查与监控

# 健康检查端点
curl http://localhost/api/health

# 监控容器资源使用
docker stats

# 查看数据库连接
docker-compose exec db mysql -u user -p -e "SHOW PROCESSLIST"

6.3 备份策略

# 备份MySQL数据
docker-compose exec db mysqldump -u user -p app_db > backup_$(date +%Y%m%d).sql

# 备份Docker Volume
docker run --rm -v app_mysql_data:/data -v $(pwd):/backup alpine tar czf /backup/mysql_backup.tar.gz -C /data .

6.4 生产环境优化清单

  1. 启用HTTPS:使用Let’s Encrypt + Certbot自动获取SSL证书
  2. 限流保护:在Nginx中配置rate limiting
  3. 日志轮转:配置Docker日志驱动为json-file并设置max-size
  4. 资源限制:在docker-compose.yml中设置CPU和内存限制
  5. 秘密管理:使用Docker Secrets或外部密钥管理服务
  6. 监控告警:集成Prometheus + Grafana

七、常见问题排查

问题1:后端无法连接数据库

# 检查数据库是否就绪
docker-compose exec backend python -c "
import pymysql
conn = pymysql.connect(host='db', user='user', password='password', database='app_db')
print('连接成功')
conn.close()
"

问题2:前端API请求404

检查Nginx的proxy_pass配置,确保URL路径正确映射。特别注意proxy_pass末尾的斜杠。

问题3:容器内存泄漏

使用docker stats监控内存使用,在docker-compose.yml中设置mem_limitmem_reservation

八、总结

通过Docker Compose + Nginx的部署方案,我们实现了一套完整的生产级全栈应用部署架构。这套方案的核心优势在于:

  • 环境一致性:Docker确保开发、测试、生产环境完全一致
  • 一键部署docker-compose up -d即可启动整个应用栈
  • 服务隔离:每个服务在独立容器中运行,互不干扰
  • 水平扩展:通过docker-compose up -d --scale backend=3轻松扩展后端实例
  • 数据持久化:Docker Volume确保数据在容器重启后不丢失

这套方案可以直接作为新项目的部署模板,只需替换业务代码和配置即可快速上线。

Logo

小龙虾开发者社区是 CSDN 旗下专注 OpenClaw 生态的官方阵地,聚焦技能开发、插件实践与部署教程,为开发者提供可直接落地的方案、工具与交流平台,助力高效构建与落地 AI 应用

更多推荐