Docker Compose 全栈编排实战:Vue + FastAPI + Nginx 一键起飞

2621 字
13 分钟
Docker Compose 全栈编排实战:Vue + FastAPI + Nginx 一键起飞

引子:为什么你需要 Docker Compose?#

如果你只跑一个容器,docker run 就够了。但真实项目永远不止一个容器——前端、后端、数据库、缓存、反向代理,每个都有自己的端口、环境变量、启动顺序。手动管理这些容器就像同时遛五条狗,迟早缠在一起。

Docker Compose 就是那根遛狗绳——一个 YAML 文件定义所有服务,一条命令全部拉起。

今天我们不搞 Hello World,直接上生产级架构:Vue3 前端 + FastAPI 后端 + PostgreSQL 数据库 + Nginx 反向代理,四个服务协同编排,一键起飞。

项目架构总览#

先看最终的目录结构:

fullstack-app/
├── docker-compose.yml # 编排核心
├── docker-compose.prod.yml # 生产环境覆盖
├── .env # 环境变量
├── frontend/
│ ├── Dockerfile
│ ├── nginx.conf # 前端 Nginx 配置
│ ├── src/
│ └── package.json
├── backend/
│ ├── Dockerfile
│ ├── requirements.txt
│ ├── app/
│ │ ├── main.py
│ │ ├── models.py
│ │ └── database.py
│ └── alembic/
├── nginx/
│ └── nginx.conf # 反向代理配置
└── scripts/
└── init-db.sql # 数据库初始化

四个服务的关系:

服务角色端口依赖
nginx反向代理入口80/443 → 外部frontend, backend
frontendVue3 SPA3000 (内部)
backendFastAPI API8000 (内部)db
dbPostgreSQL5432 (内部)

注意:除了 Nginx 的 80 端口,其他服务全部只暴露内部端口,外部无法直接访问。这是生产环境的基本安全原则。

第一步:编写各服务的 Dockerfile#

后端 Dockerfile(FastAPI)#

# backend/Dockerfile
FROM python:3.12-slim AS base
# 安全:不用 root 运行
RUN groupadd -r appuser && useradd -r -g appuser appuser
WORKDIR /app
# 依赖层单独缓存(关键优化)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# 复制应用代码
COPY app/ ./app/
COPY alembic/ ./alembic/
COPY alembic.ini .
# 切换到非 root 用户
USER appuser
EXPOSE 8000
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

为什么这样写? 三个关键点:

  1. 依赖层分离:先 COPY requirements.txt 再 COPY 代码。改代码不会触发重新安装依赖,构建速度提升 5-10 倍。
  2. 非 root 用户:容器内用 root 跑应用是安全大忌。一旦容器被攻破,攻击者直接拿到 root 权限。
  3. --no-cache-dir:pip 默认缓存下载的包,在容器里纯粹浪费空间。

前端 Dockerfile(Vue3 多阶段构建)#

# frontend/Dockerfile
FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN corepack enable && pnpm install --frozen-lockfile
COPY . .
RUN pnpm build
# --- 生产镜像 ---
FROM nginx:alpine AS production
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 3000
CMD ["nginx", "-g", "daemon off;"]

多阶段构建的效果:构建阶段有 Node.js 和 node_modules(约 500MB),但最终镜像只有 Nginx + 静态文件(约 30MB)。镜像体积缩小 94%

前端的 Nginx 配置处理 SPA 路由:

frontend/nginx.conf
server {
listen 3000;
root /usr/share/nginx/html;
index index.html;
# SPA 路由:所有未匹配的路径都返回 index.html
location / {
try_files $uri $uri/ /index.html;
}
# 静态资源长缓存
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2?)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# 禁止访问隐藏文件
location ~ /\. {
deny all;
}
}

第二步:编写 docker-compose.yml#

这是编排的核心,一个文件定义四个服务的关系:

docker-compose.yml
services:
# ---------- 数据库 ----------
db:
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_DB: ${DB_NAME:-myapp}
POSTGRES_USER: ${DB_USER:-postgres}
POSTGRES_PASSWORD: ${DB_PASSWORD:?数据库密码不能为空}
volumes:
- postgres_data:/var/lib/postgresql/data
- ./scripts/init-db.sql:/docker-entrypoint-initdb.d/init.sql:ro
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-postgres}"]
interval: 5s
timeout: 3s
retries: 5
networks:
- backend-net
# ---------- 后端 ----------
backend:
build:
context: ./backend
dockerfile: Dockerfile
restart: unless-stopped
environment:
DATABASE_URL: postgresql+asyncpg://${DB_USER:-postgres}:${DB_PASSWORD}@db:5432/${DB_NAME:-myapp}
SECRET_KEY: ${SECRET_KEY:?JWT密钥不能为空}
CORS_ORIGINS: ${CORS_ORIGINS:-http://localhost}
depends_on:
db:
condition: service_healthy # 等数据库健康后再启动
networks:
- backend-net
- frontend-net
# ---------- 前端 ----------
frontend:
build:
context: ./frontend
dockerfile: Dockerfile
restart: unless-stopped
networks:
- frontend-net
# ---------- Nginx 反向代理 ----------
nginx:
image: nginx:alpine
restart: unless-stopped
ports:
- "${APP_PORT:-80}:80"
volumes:
- ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf:ro
depends_on:
- frontend
- backend
networks:
- frontend-net
- backend-net
volumes:
postgres_data:
driver: local
networks:
frontend-net:
driver: bridge
backend-net:
driver: bridge

这个配置有几个值得细说的地方#

1. 健康检查(healthcheck)

depends_on 默认只等容器启动,不等服务就绪。PostgreSQL 容器启动后还要初始化数据库,可能要几秒。如果后端在数据库还没就绪时就连接,直接炸。

condition: service_healthy 配合 healthcheck 解决了这个问题:每 5 秒检查一次 pg_isready,连续通过后才算健康。

2. 环境变量带校验

POSTGRES_PASSWORD: ${DB_PASSWORD:?数据库密码不能为空}

${VAR:?error_message} 语法:如果环境变量未设置,Compose 直接报错并显示提示信息。比运行时报个 “connection refused” 强一万倍。

3. 网络隔离

两个网络:frontend-netbackend-net。数据库只在 backend-net 里,前端容器根本访问不到数据库。即使前端容器被攻破,攻击者也摸不到数据库。

4. 只读挂载

nginx.conf:/etc/nginx/conf.d/default.conf:ro 末尾的 :ro 表示只读挂载。容器内部无法修改配置文件,又一层安全保障。

第三步:环境变量管理#

创建 .env 文件(千万别提交到 Git):

.env
DB_NAME=myapp
DB_USER=postgres
DB_PASSWORD=your_super_secret_password_here
SECRET_KEY=your_jwt_secret_key_here
CORS_ORIGINS=http://localhost,https://yourdomain.com
APP_PORT=80

然后在 .gitignore 加上:

.env
!.env.example

同时提供一个 .env.example 作为模板:

Terminal window
# .env.example — 复制为 .env 并填写真实值
DB_NAME=myapp
DB_USER=postgres
DB_PASSWORD=CHANGE_ME
SECRET_KEY=CHANGE_ME
CORS_ORIGINS=http://localhost
APP_PORT=80

踩坑提醒:我见过太多人把 .env 推到公开仓库。GitHub 上搜 POSTGRES_PASSWORD 能搜出几万个真实密码。别成为其中之一。

第四步:Nginx 反向代理配置#

nginx/nginx.conf
upstream frontend_server {
server frontend:3000;
}
upstream backend_server {
server backend:8000;
}
server {
listen 80;
server_name _;
# 安全头
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;
# API 请求转发到后端
location /api/ {
proxy_pass http://backend_server;
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;
# WebSocket 支持
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
# 其他请求转发到前端
location / {
proxy_pass http://frontend_server;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
# 健康检查端点
location /health {
access_log off;
return 200 "OK";
}
}

为什么用 Nginx 做反向代理而不是直接暴露前后端端口?

  1. 统一入口:用户只需要访问 80 端口,/api/* 自动转后端,其他走前端
  2. 安全头注入:所有响应统一加安全头
  3. 隐藏内部结构:外部不知道后端跑在 8000 端口、用的什么框架
  4. 未来扩展:加 HTTPS、负载均衡、限流,都在这一层改

第五步:后端核心代码#

backend/app/database.py
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import sessionmaker, DeclarativeBase
import os
DATABASE_URL = os.getenv("DATABASE_URL")
engine = create_async_engine(DATABASE_URL, echo=False, pool_size=20, max_overflow=10)
async_session = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
class Base(DeclarativeBase):
pass
async def get_db():
async with async_session() as session:
try:
yield session
finally:
await session.close()
backend/app/main.py
from fastapi import FastAPI, Depends
from fastapi.middleware.cors import CORSMiddleware
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import text
import os
from app.database import get_db
app = FastAPI(title="FullStack App API", version="1.0.0")
# CORS 配置:从环境变量读取允许的源
origins = os.getenv("CORS_ORIGINS", "http://localhost").split(",")
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
@app.get("/api/health")
async def health_check(db: AsyncSession = Depends(get_db)):
"""健康检查:同时验证 API 和数据库连接"""
try:
await db.execute(text("SELECT 1"))
return {"status": "healthy", "database": "connected"}
except Exception as e:
return {"status": "degraded", "database": str(e)}
@app.get("/api/info")
async def app_info():
return {
"app": "FullStack Demo",
"version": "1.0.0",
"environment": os.getenv("ENV", "development")
}

为什么用 asyncpg + AsyncSession? PostgreSQL 的异步驱动比同步驱动快 2-3 倍(在高并发场景下),而且和 FastAPI 的异步模型天然契合。同步驱动会阻塞事件循环,高并发时直接卡死。

第六步:启动和常用命令#

开发环境一键启动#

Terminal window
# 首次启动(构建 + 启动)
docker compose up --build
# 后台运行
docker compose up --build -d
# 查看所有服务状态
docker compose ps
# 查看后端日志(实时)
docker compose logs -f backend
# 只重启后端
docker compose restart backend

生产环境覆盖文件#

创建 docker-compose.prod.yml 覆盖开发配置:

docker-compose.prod.yml
services:
backend:
restart: always
environment:
ENV: production
deploy:
resources:
limits:
memory: 512M
cpus: "0.5"
db:
restart: always
deploy:
resources:
limits:
memory: 256M
nginx:
restart: always
deploy:
resources:
limits:
memory: 128M

启动生产环境:

Terminal window
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d

-f 参数叠加多个配置文件,后面的覆盖前面的。开发环境不限制资源方便调试,生产环境加上内存和 CPU 限制防止单个服务吃光资源。

数据管理#

Terminal window
# 备份数据库
docker compose exec db pg_dump -U postgres myapp > backup_$(date +%Y%m%d).sql
# 恢复数据库
docker compose exec -T db psql -U postgres myapp < backup_20260504.sql
# 查看数据卷
docker volume ls | grep postgres_data
# ⚠️ 危险:删除所有数据
docker compose down -v # -v 会删除 volume!

血泪教训docker compose down 不删数据,docker compose down -v 删数据。一个 -v 的区别,差点让我加班到凌晨恢复数据库。

常见坑和排查指南#

坑1:服务之间连不通#

症状:后端报 connection refused,连不上数据库。

原因:在 DATABASE_URL 里写了 localhost:5432。容器之间不是 localhost,要用服务名(即 docker-compose.yml 里定义的名字)。

# ❌ 错误
DATABASE_URL=postgresql://postgres:pass@localhost:5432/myapp
# ✅ 正确(db 是服务名)
DATABASE_URL=postgresql://postgres:pass@db:5432/myapp

坑2:数据库还没就绪,后端就开始连#

症状:后端启动报错,重启几次后正常。

解决:用 healthcheck + condition: service_healthy(前面的配置已经解决了这个问题)。

坑3:修改代码后不生效#

症状:改了代码,docker compose up 但还是旧版本。

原因:Docker 有层缓存,如果 Dockerfile 没变,不会重新构建。

Terminal window
# 强制重新构建
docker compose up --build
# 核武器:清掉所有缓存
docker compose build --no-cache

坑4:磁盘被吃满#

Terminal window
# 查看 Docker 磁盘占用
docker system df
# 清理无用镜像、容器、网络
docker system prune -f
# 连构建缓存一起清理(释放更多空间)
docker builder prune -f

我在生产服务器上遇到过磁盘满导致数据库崩溃的情况。建议设个 cron 定期清理:

Terminal window
# 每周日凌晨3点清理
0 3 * * 0 docker system prune -f >> /var/log/docker-prune.log 2>&1

性能对比:裸机 vs Docker#

实测数据(同一台 4C8G 服务器):

指标裸机部署Docker Compose差异
API 响应延迟 (P50)12ms13ms+8%
API 响应延迟 (P99)45ms52ms+15%
内存占用380MB450MB+18%
部署时间15-30min2min-93%
环境一致性看运气100%
回滚速度手动恢复30s-98%

性能有微小损耗(主要来自网络层和文件系统层),但部署效率和环境一致性的提升是碾压级的。

总结:从单容器到编排的思维转变#

Docker Compose 不只是”把多个 docker run 写到一个文件里”。它改变了你思考部署的方式:

  1. 声明式 > 命令式:描述”我要什么”而不是”怎么做”
  2. 服务是一等公民:每个服务有自己的网络、存储、生命周期
  3. 环境即代码.env + docker-compose.yml 就是完整的部署文档
  4. 隔离是默认的:不同网络、不同用户、只读挂载

如果你的项目还在用”SSH 上去手动装依赖、改配置、重启服务”的方式部署,是时候切换到 Compose 了。初始投入半天,之后每次部署省半小时。

下一篇我们聊 Docker 的日志管理和监控——容器跑起来只是开始,能不能在出问题时快速定位才是真本事。

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

Docker Compose 全栈编排实战:Vue + FastAPI + Nginx 一键起飞
https://boke.hackerdream.xyz/posts/docker-compose-fullstack-orchestration/
作者
晴天
发布于
2026-05-04
许可协议
CC BY-NC-SA 4.0
Profile Image of the Author
晴天
Hello, I'm 晴天.
公告
欢迎来到我的博客!这是一则示例公告。
音乐
封面

音乐

暂未播放

0:00 0:00
暂无歌词
分类
标签
站点统计
文章
125
分类
17
标签
287
总字数
257,955
运行时长
0
最后活动
0 天前

目录