Python全栈实战:Docker Compose + Nginx 部署 FastAPI、Vue3 与 MySQL
·
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 生产环境优化清单
- 启用HTTPS:使用Let’s Encrypt + Certbot自动获取SSL证书
- 限流保护:在Nginx中配置rate limiting
- 日志轮转:配置Docker日志驱动为json-file并设置max-size
- 资源限制:在docker-compose.yml中设置CPU和内存限制
- 秘密管理:使用Docker Secrets或外部密钥管理服务
- 监控告警:集成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_limit和mem_reservation。
八、总结
通过Docker Compose + Nginx的部署方案,我们实现了一套完整的生产级全栈应用部署架构。这套方案的核心优势在于:
- 环境一致性:Docker确保开发、测试、生产环境完全一致
- 一键部署:
docker-compose up -d即可启动整个应用栈 - 服务隔离:每个服务在独立容器中运行,互不干扰
- 水平扩展:通过
docker-compose up -d --scale backend=3轻松扩展后端实例 - 数据持久化:Docker Volume确保数据在容器重启后不丢失
这套方案可以直接作为新项目的部署模板,只需替换业务代码和配置即可快速上线。
更多推荐



所有评论(0)