FastAPI + Vue3 类型项目 Docker 部署完整教程
以 "AI掘金头条" 新闻资讯项目为例,涵盖从零到一的 Docker Compose 部署全流程,包括常见踩坑与解决方案。
快速开始(老手直接看这里)
如果对 Docker 熟悉,直接复制下面文件即可部署。
前提:服务器已装 Docker + Docker Compose,项目目录按下方结构放好。
bash
# 1. 确保项目根目录有这些文件:
# docker-compose.yml、database.sql
# toutiao_backend/Dockerfile、toutiao_backend/requirements.txt
# xwzx-news/Dockerfile、xwzx-news/nginx.conf、xwzx-news/.env.production
# 2. 启动
cd /opt/fastapi
docker compose up -d --build
# 3. 验证
curl http://192.168.194.10/api/news/categories
三个最关键的注意点(不搞清楚必踩坑):
api.js中baseURL用??不用||,否则空字符串会走 fallbackrequirements.txt必须包含cryptography,否则 MySQL 8.0 连不上- MySQL command 必须加
--skip-character-set-client-handshake,否则中文乱码
下面是从原理到实践的完整讲解。
目录
- 一、项目架构概览
- [二、准备工作:让代码适配 Docker](#二、准备工作:让代码适配 Docker)
- [三、编写 Dockerfile](#三、编写 Dockerfile)
- [四、编写 docker-compose.yml](#四、编写 docker-compose.yml)
- 五、部署到服务器
- 六、验证与测试
- 七、常见踩坑与解决
一、项目架构概览
项目根目录/
├── docker-compose.yml # 服务编排
├── database.sql # 数据库初始化脚本
├── toutiao_backend/ # FastAPI 后端
│ ├── Dockerfile
│ ├── requirements.txt
│ ├── main.py
│ ├── config/
│ │ ├── db_config.py # MySQL 配置
│ │ └── cache_conf.py # Redis 配置
│ ├── models/ # ORM 模型
│ ├── routers/ # API 路由
│ ├── crud/ # 数据库操作
│ ├── schemas/ # Pydantic 模型
│ └── utils/ # 工具函数
└── xwzx-news/ # Vue3 前端
├── Dockerfile
├── nginx.conf # Nginx 反向代理配置
├── .env.production # 生产环境变量
├── vite.config.js
└── src/
└── config/
└── api.js # API 地址配置
容器拓扑结构:
浏览器 ──→ Nginx:80 (frontend)
│
├── / → 静态文件 (Vue SPA)
└── /api/* → 反向代理 → FastAPI:8000 (backend)
│
├── MySQL:3306
└── Redis:6379
Docker 网络通信原理(理解这个才能排查连接问题):
Docker Compose 会为所有服务创建一个默认网络 ,每个容器自动获得以服务名为域名的内部 DNS。例如:
bash
# 在后端容器中,可以直接通过服务名访问其他容器:
ping mysql # 解析到 MySQL 容器 IP(172.18.0.x)
ping redis # 解析到 Redis 容器 IP
这就是为什么配置文件里写 DB_HOST=mysql、REDIS_HOST=redis 而不是 IP 地址。
| 通信场景 | 地址写法 | 说明 |
|---|---|---|
| 前端 → 后端(Nginx 代理) | http://backend:8000 |
容器内部通信 |
| 后端 → MySQL | DB_HOST=mysql |
环境变量注入 |
| 后端 → Redis | REDIS_HOST=redis |
环境变量注入 |
| 浏览器 → 前端 | http://192.168.194.10:80 |
外部访问,走端口映射 |
| 容器内访问宿主机 | host.docker.internal |
特殊域名(Docker 20.10+) |
二、准备工作:让代码适配 Docker
源码中数据库和 Redis 连接地址是硬编码的 localhost,容器化后需要通过环境变量动态注入。
2.1 修改数据库配置
文件:toutiao_backend/config/db_config.py
修改前:硬编码连接字符串
pythonASYNC_DATABASE_URL = "mysql+aiomysql://root:123456@localhost:3306/news_app?charset=utf8mb4"
修改后:
python
import os
from sqlalchemy.ext.asyncio import async_sessionmaker, AsyncSession, create_async_engine
# 从环境变量读取,本地开发用默认值 localhost
DB_HOST = os.getenv("DB_HOST", "localhost")
DB_PORT = os.getenv("DB_PORT", "3306")
DB_USER = os.getenv("DB_USER", "root")
DB_PASSWORD = os.getenv("DB_PASSWORD", "123456")
DB_NAME = os.getenv("DB_NAME", "news_app")
ASYNC_DATABASE_URL = f"mysql+aiomysql://{DB_USER}:{DB_PASSWORD}@{DB_HOST}:{DB_PORT}/{DB_NAME}?charset=utf8mb4"
# 后续代码保持不变 ...
关键原则 :os.getenv("KEY", "默认值") 确保本地开发直接 python main.py 也能跑,Docker 部署时通过环境变量覆盖。
2.2 修改 Redis 配置
文件:toutiao_backend/config/cache_conf.py
python
import json
import os
from typing import Any
import redis.asyncio as redis
REDIS_HOST = os.getenv("REDIS_HOST", "localhost")
REDIS_PORT = int(os.getenv("REDIS_PORT", "6379"))
REDIS_DB = int(os.getenv("REDIS_DB", "0"))
# 后续代码保持不变 ...
2.3 修改前端 API 地址配置
文件:xwzx-news/src/config/api.js
核心改动:
||换成??,||会把空字符串判为 false 导致走误 fallback!
javascript
// 开发环境: http://127.0.0.1:8000 (直连后端)
// Docker部署: 空字符串 (Nginx反向代理 /api/ → backend)
export const apiConfig = {
baseURL: import.meta.env.VITE_API_BASE_URL ?? 'http://127.0.0.1:8000',
}
为什么用 ?? 而不是 ||?
| 值 | "" || fallback | "" ?? fallback |
|----------------|------------------|------------------|
| "" (空字符串) | fallback ← 错误! | "" ← 正确 |
| undefined | fallback | fallback |
| null | fallback | fallback |
| "http://..." | "http://..." | "http://..." |
Docker 构建时需要把
VITE_API_BASE_URL设为空字符串 (让请求走同源 Nginx 代理),空字符串是 falsy 值,所以必须用??。
2.4 添加 Vite 开发代理(可选)
文件:xwzx-news/vite.config.js
javascript
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
server: {
proxy: {
'/api': {
target: 'http://127.0.0.1:8000',
changeOrigin: true,
},
},
},
})
加了这个之后,本地
npm run dev也能用相对路径/api/xxx,不需要直连 8000 端口,避免跨域问题。
2.5 添加 .dockerignore(加快构建速度)
Docker 构建时会把整个上下文目录发给 Docker daemon。不加 .dockerignore 会把 .venv、node_modules、__pycache__ 等大文件也传过去,严重影响构建速度。
文件:toutiao_backend/.dockerignore
__pycache__
*.pyc
.venv
.env
.git
.gitignore
*.md
test_*
.idea
cache
文件:xwzx-news/.dockerignore
node_modules
dist
.git
.gitignore
*.md
.env
.env.local
注意:
.env.production不能被忽略,构建时需要它。
三、编写 Dockerfile
3.1 后端 Dockerfile
文件:toutiao_backend/Dockerfile
dockerfile
FROM python:3.12-slim
WORKDIR /app
# 安装 gcc(bcrypt 编译需要)
RUN apt-get update && apt-get install -y --no-install-recommends \
gcc \
&& rm -rf /var/lib/apt/lists/*
# 先复制依赖文件(利用 Docker 缓存层,代码改动时不用重新 pip install)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
# 再复制应用代码
COPY . .
EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
要点说明:
python:3.12-slim--- 体积小,够用gcc---bcrypt库编译需要,否则安装会报错- 先
COPY requirements.txt再COPY . .--- Docker 分层缓存优化,改代码不用重新装依赖 --host 0.0.0.0--- 必须,否则容器外无法访问
3.2 后端依赖文件
文件:toutiao_backend/requirements.txt
fastapi>=0.115.0
uvicorn[standard]>=0.30.0
sqlalchemy[asyncio]>=2.0.0
aiomysql>=0.2.0
redis>=5.0.0
passlib[bcrypt]==1.7.4
bcrypt==3.2.2
pymysql>=1.1.0
cryptography>=41.0.0
python-multipart>=0.0.9
重要 :
cryptography必须加!MySQL 8.0 默认使用caching_sha2_password认证,没有这个包会报RuntimeError: 'cryptography' package is required。
3.3 前端 Dockerfile
文件:xwzx-news/Dockerfile
dockerfile
# 阶段一:Node 构建
FROM node:22-alpine AS builder
WORKDIR /app
COPY package.json ./
RUN npm install
COPY . .
# VITE_API_BASE_URL 为空字符串 = 前端请求走同源 Nginx 代理
ARG VITE_API_BASE_URL=
RUN VITE_API_BASE_URL=${VITE_API_BASE_URL} npm run build
# 阶段二:Nginx 运行
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;"]
多阶段构建的好处:
- 阶段一:Node 环境,只用于编译
- 阶段二:Nginx 环境,只包含编译产物
- 最终镜像体积很小(~20MB),不含
node_modules
3.4 Nginx 配置
文件:xwzx-news/nginx.conf
nginx
server {
listen 80;
server_name localhost;
# Vue SPA 静态文件
location / {
root /usr/share/nginx/html;
index index.html;
try_files $uri $uri/ /index.html; # history 模式路由支持
}
# API 反向代理到后端容器
location /api/ {
proxy_pass http://backend:8000; # backend = docker-compose 服务名
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;
}
}
关键点:
try_files $uri $uri/ /index.html--- Vue Router history 模式必须,否则刷新 404proxy_pass http://backend:8000---backend是 docker-compose 中的服务名,Docker 内部 DNS 自动解析- 不暴露 8000 端口给外部 --- 所有请求走 80
3.5 前端生产环境变量
文件:xwzx-news/.env.production
VITE_API_BASE_URL=
Vite 在
vite build时自动加载.env.production,把VITE_API_BASE_URL设为空字符串。
四、编写 docker-compose.yml
文件:项目根目录 docker-compose.yml
yaml
services:
# ============ MySQL ============
mysql:
image: mysql:8.0
container_name: news_mysql
restart: always
environment:
MYSQL_ROOT_PASSWORD: 123456
MYSQL_DATABASE: news_app
MYSQL_CHARSET: utf8mb4
MYSQL_COLLATION: utf8mb4_unicode_ci
ports:
- "3306:3306"
volumes:
- mysql_data:/var/lib/mysql # 数据持久化
- ./database.sql:/docker-entrypoint-initdb.d/init.sql # 自动建表
command:
- --character-set-server=utf8mb4
- --collation-server=utf8mb4_unicode_ci
- --init-connect=SET NAMES utf8mb4 # 每个连接也强制 utf8mb4
- --skip-character-set-client-handshake # 跳过客户端字符集协商
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p123456"]
interval: 10s
timeout: 5s
retries: 10
# ============ Redis ============
redis:
image: redis:7-alpine
container_name: news_redis
restart: always
ports:
- "6379:6379"
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
# ============ FastAPI 后端 ============
backend:
build: ./toutiao_backend
container_name: news_backend
restart: always
ports:
- "8000:8000"
depends_on:
mysql:
condition: service_healthy # 等 MySQL 健康检查通过才启动
redis:
condition: service_healthy
environment:
- DB_HOST=mysql # 容器名 = 主机名
- DB_PORT=3306
- DB_USER=root
- DB_PASSWORD=123456
- DB_NAME=news_app
- REDIS_HOST=redis
- REDIS_PORT=6379
# ============ Vue 前端 ============
frontend:
build:
context: ./xwzx-news
args:
VITE_API_BASE_URL: "" # 空字符串 = 走 Nginx 代理
container_name: news_frontend
restart: always
ports:
- "80:80"
depends_on:
- backend
volumes:
mysql_data:
配置解读:
| 配置项 | 作用 |
|---|---|
restart: always |
容器崩溃自动重启,开机自启 |
depends_on > condition: service_healthy |
等待依赖服务就绪,避免启动顺序问题 |
volumes: mysql_data |
数据卷持久化,容器删除数据不丢 |
./database.sql:/docker-entrypoint-initdb.d/init.sql |
MySQL 首次启动自动执行建表 |
--skip-character-set-client-handshake |
跳过客户端字符集协商,强制 utf8mb4 |
为什么需要 skip-character-set-client-handshake?
MySQL 8.0 容器即使设置了 character-set-server=utf8mb4,客户端连接时仍可能协商成 latin1,导致中文乱码(显示为 央行 等乱码)。加上这个参数彻底锁定字符集。
五、部署到服务器
5.1 服务器环境要求
- 操作系统:CentOS 7+ / Red Hat / Ubuntu
- 已安装 Docker 和 Docker Compose
bash
# Red Hat / CentOS 安装 Docker
sudo dnf config-manager --add-repo https://download.docker.com/linux/rhel/docker-ce.repo
sudo dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
# 启动
sudo systemctl start docker
sudo systemctl enable docker
5.2 传输项目文件
将整个项目目录上传到服务器(排除 node_modules、.venv、__pycache__):
bash
# 本地先清理
rm -rf xwzx-news/node_modules toutiao_backend/.venv toutiao_backend/__pycache__
# 上传到服务器
scp -r ./fastapi root@192.168.194.10:/opt/
5.3 一键启动
bash
cd /opt/fastapi
docker compose up -d --build
首次启动 MySQL 会自动执行 database.sql 建表(约 1-2 分钟)。
5.4 常用管理命令
bash
# 查看运行状态
docker compose ps
# 查看日志
docker compose logs -f # 所有服务
docker compose logs -f backend # 指定服务
# 重启
docker compose restart
# 停止
docker compose stop
# 停止并删除容器(保留数据)
docker compose down
# 停止并删除容器 + 数据卷(⚠ 数据库数据会丢失)
docker compose down -v
# 重新构建并启动
docker compose up -d --build
# 只重建单个服务
docker compose build --no-cache frontend
docker compose up -d frontend
5.5 更新代码后如何重新部署
日常开发中改代码 → 重新部署是最频繁的操作。不同改动的更新方式不同:
只改了前端代码(页面、样式、API 调用):
bash
cd /opt/fastapi
docker compose build --no-cache frontend
docker compose up -d frontend
只改了后端代码(路由、CRUD、逻辑):
bash
cd /opt/fastapi
docker compose build --no-cache backend
docker compose up -d backend
改了 requirements.txt 或 Dockerfile(依赖变更):
bash
cd /opt/fastapi
docker compose build --no-cache backend
docker compose up -d backend
改了 database.sql(数据库结构变更):
bash
cd /opt/fastapi
docker compose down -v # 删数据卷重建
docker compose up -d --build
改了 docker-compose.yml(服务配置变更):
bash
cd /opt/fastapi
docker compose up -d --build # Docker 会自己判断哪些需要重建
小技巧:如果不是确定需要
--no-cache,先试docker compose up -d --build,它只重建有变化的层,快很多。
六、验证与测试
6.1 浏览器访问
http://192.168.194.10 → 前端页面
http://192.168.194.10/api/ → 后端接口(通过 Nginx 代理)
http://192.168.194.10:8000/ → 后端接口(直连)
6.2 命令行接口测试
bash
# 测试 API
curl http://192.168.194.10/api/news/categories
# 测试注册
curl -X POST http://192.168.194.10/api/user/register \
-H "Content-Type: application/json" \
-d '{"username":"test","password":"123456"}'
# 测试 + 查看后端日志(排查问题用)
curl -v -X POST http://192.168.194.10/api/user/register \
-H "Content-Type: application/json" \
-d '{"username":"test","password":"123456"}'; \
docker logs news_backend --tail 20
6.3 验证数据库
bash
# 进入 MySQL 容器
docker exec -it news_mysql mysql -uroot -p123456 news_app
# 查看表
SHOW TABLES;
# 验证中文字符集
SHOW VARIABLES LIKE 'character%';
七、常见踩坑与解决
坑 1:cryptography package is required
RuntimeError: 'cryptography' package is required for sha256_password or caching_sha2_password auth methods
原因 :MySQL 8.0 默认认证插件 caching_sha2_password 需要 cryptography 包。
解决 :requirements.txt 加入 cryptography>=41.0.0。
坑 2:前端请求 127.0.0.1:8000 连接拒绝
Failed to load resource: net::ERR_CONNECTION_REFUSED
http://127.0.0.1:8000/api/user/register
原因 :前端构建时 VITE_API_BASE_URL 没生效,走了 fallback 值。
排查方法:
bash
# 检查构建产物中是否还有 127.0.0.1
docker exec news_frontend sh -c "grep -r '127.0.0.1:8000' /usr/share/nginx/html/"
根因 :api.js 中用了 || 运算符:
javascript
baseURL: import.meta.env.VITE_API_BASE_URL || 'http://127.0.0.1:8000'
// ^^ 空字符串是 falsy,会走 fallback!
解决 :改用 ??(空值合并运算符):
javascript
baseURL: import.meta.env.VITE_API_BASE_URL ?? 'http://127.0.0.1:8000'
// ^^ 只有 null/undefined 才走 fallback
坑 3:中文乱码(央行宣å¸)
原因 :MySQL 容器字符集协商问题,客户端连接降级为 latin1。
排查方法:
bash
docker exec news_mysql mysql -uroot -p123456 -e "SHOW VARIABLES LIKE 'character%';"
解决 :docker-compose.yml MySQL command 加两行:
yaml
command:
- --character-set-server=utf8mb4
- --collation-server=utf8mb4_unicode_ci
- --init-connect=SET NAMES utf8mb4 # 新增
- --skip-character-set-client-handshake # 新增
然后重建(⚠ 会清空数据):
bash
docker compose down -v
docker compose up -d --build
坑 4:Docker 构建缓存导致修改不生效
现象:改了代码但构建后还是旧逻辑。
解决:
bash
# 强制无缓存重建
docker compose build --no-cache frontend
docker compose up -d frontend
坑 5:bcrypt 版本兼容性问题
Python 3.12 + bcrypt 最新版可能编译失败或 API 不兼容。
解决:锁定版本:
passlib[bcrypt]==1.7.4
bcrypt==3.2.2
坑 6:Vue Router 刷新 404
原因 :Nginx 没有配置 try_files,非根路径刷新时找不到文件。
解决 :nginx.conf 中:
nginx
location / {
try_files $uri $uri/ /index.html; # 这句必须有
}
附录:文件清单与路径
| 文件 | 路径 | 说明 |
|---|---|---|
| docker-compose.yml | 项目根目录 | 服务编排 |
| Dockerfile(后端) | toutiao_backend/Dockerfile |
FastAPI 镜像 |
| requirements.txt | toutiao_backend/requirements.txt |
Python 依赖 |
| db_config.py | toutiao_backend/config/db_config.py |
需改环境变量 |
| cache_conf.py | toutiao_backend/config/cache_conf.py |
需改环境变量 |
| Dockerfile(前端) | xwzx-news/Dockerfile |
多阶段构建 |
| nginx.conf | xwzx-news/nginx.conf |
Nginx 代理 |
| .env.production | xwzx-news/.env.production |
Vite 生产环境变量 |
| api.js | xwzx-news/src/config/api.js |
` |
| vite.config.js | xwzx-news/vite.config.js |
开发代理配置 |
| .dockerignore(后端) | toutiao_backend/.dockerignore |
排除 .venv 等大文件 |
| .dockerignore(前端) | xwzx-news/.dockerignore |
排除 node_modules |
| database.sql | 项目根目录 | 数据库建表脚本 |
附录 B:移植到自己的项目
这份配置不是绑定这个新闻项目的。如果你的项目也是 FastAPI 后端 + Vue3 前端 + MySQL + Redis,改动清单如下:
| 需要改的 | 文件 | 改什么 |
|---|---|---|
| 数据库密码 | docker-compose.yml |
MYSQL_ROOT_PASSWORD、DB_PASSWORD、healthcheck 中的密码 |
| 数据库连接 | db_config.py |
如果表结构不同,改 DB_NAME |
| 后端端口 | Dockerfile、docker-compose.yml、nginx.conf |
如果后端不是 8000 |
| API 路由前缀 | nginx.conf、vite.config.js |
如果 API 前缀不是 /api/ |
| 项目路径 | docker-compose.yml |
build: ./你的后端目录、build: ./你的前端目录 |
| 前端依赖 | xwzx-news/Dockerfile |
如果有额外依赖需要安装 |
| 数据库初始化 | database.sql |
换成你自己的建表 SQL |
非 MySQL? 改 requirements.txt 中的数据库驱动(如 asyncpg 替代 aiomysql),修改 db_config.py 中的连接字符串格式,改 docker-compose.yml 中数据库镜像。
非 Redis? 把相关配置删掉即可,这个项目对 Redis 是弱依赖(缓存)。
以上就是从源码到 Docker 部署的完整流程。核心思路:环境变量注入配置 → Dockerfile 定义镜像 → docker-compose 编排服务 → 一键部署。遇到问题先看日志,大部分问题都是环境变量或字符集配置引起的。