引言
为什么"能让 AI 跑起来"和"能上线的项目"之间隔着一条鸿沟
过去两年,编程世界的门槛以肉眼可见的速度坍塌。Vibe coding 让无数从未写过一行代码的人,能够对着 AI 说一句"帮我做一个记账应用",然后看着代码在屏幕上流淌,几分钟后真的得到一个能跑的页面。这无疑是程序设计民主化进程中激动人心的一刻。
但如果你仔细观察,会发现一个有趣的现象:网上绝大多数教程,终点都停在"部署一个静态页面" ------把 HTML 扔到 Vercel 上,得到一个漂亮的网址,然后呢?然后就没有然后了。
一旦你的想法稍微"真实"一点------需要用户注册登录、需要保存数据、需要数据库、需要两个服务互相通信------你会发现 AI 给你的代码突然"散架"了:数据库连不上、环境变量丢失、本地跑得好好的项目部署后疯狂报错、改了一行代码整个应用崩掉。很多 Vibe Coder 和新手程序员在这一刻陷入了一种熟悉的循环:不停地把报错信息粘贴给 AI,碰运气式地修复,直到项目在某个深夜彻底无法挽救,然后重新开始。
问题不在于 AI 不够聪明,而在于:AI 擅长生成局部正确的代码,但没有人替你回答那些"代码之外"的问题------
- 文件应该怎么组织?为什么数据库连接代码不能写在路由里?
- 为什么 Web、数据库、缓存要拆开管理,而不是塞进一个"大而全"的包里?
- 哪些文件是给机器看的、永远不该上传?哪些是项目的"档案"、必须妥善保管?
- 为什么改数据库表结构要有"迁移"这回事?
- 本地能跑和生产环境能跑,中间隔着什么?
这些恰恰是传统程序员在多年的项目中用无数次深夜事故换来的"默会知识",它们很少出现在教程里,却决定了项目是"能跑的玩具"还是"能活的系统"。在 AI 时代,这些知识不但没有贬值,反而升值了 ------因为 AI 可以替你写代码,但不能替你做架构决策;你可以不会手写 JWT 认证,但你需要知道它在整个系统中的位置。
所以这篇文章想做一件不太一样的事:走完一条完整的全景路线 。从一个空文件夹开始,用 React + FastAPI + PostgreSQL 这个最经典的全栈组合,一路走到生产环境部署------覆盖目录结构、代码分层、容器化、Git 工作流、代码规范、测试、CI/CD 的每一个环节,并且解释每一步"为什么这么做" 。
读完之后,无论你是 Vibe Coder 还是新手程序员,你和 AI 的协作方式都会发生质变:你不再是一个只会复制报错的"许愿者",而是一个知道系统全貌、能提出正确问题、能审查 AI 产出 的指挥者。AI 负责写代码,你负责让这个项目真正活下来。
这不是又一篇"十分钟部署静态页面"的教程------这是我们缺了很久的那一课。
技术栈:React (前端) + FastAPI (后端) + PostgreSQL (数据库) + Docker Compose (本地) + GitHub Actions (CI/CD) + 云服务器部署
第一部分:项目规划与仓库策略
1.1 Monorepo vs Polyrepo
对于中小型全栈项目,推荐 Monorepo(单仓库):
myapp/ # 单一 Git 仓库
├── backend/ # FastAPI
├── frontend/ # React
├── docker-compose.yml
├── .github/workflows/ # CI/CD
└── README.md
理由:版本同步简单(前端 API 调用和后端接口一起改)、一次 PR 全局评审、CI 配置集中。只有团队规模大、部署节奏完全不同时才拆分仓库。
1.2 创建仓库与初始提交
bash
mkdir myapp && cd myapp
git init -b main
# 创建 GitHub 远程仓库后
git remote add origin git@github.com:yourname/myapp.git
第一笔提交只放基础文件 (提交信息遵循 Conventional Commits 规范):
bash
git commit -m "chore: initialize repository with gitignore and readme"
第二部分:目录结构设计(核心,请仔细看)
myapp/
├── .gitignore
├── .editorconfig
├── .env.example # 环境变量模板(真实 .env 永不入库)
├── README.md
├── Makefile # 常用命令快捷方式
├── docker-compose.yml # 本地开发编排
├── docker-compose.prod.yml # 生产覆盖配置
├── .github/
│ └── workflows/
│ ├── backend-ci.yml
│ ├── frontend-ci.yml
│ └── deploy.yml
│
├── backend/
│ ├── Dockerfile
│ ├── .dockerignore
│ ├── pyproject.toml # 用 uv/poetry 管理依赖(不用 requirements.txt 裸奔)
│ ├── .python-version
│ ├── alembic.ini # 数据库迁移
│ ├── alembic/
│ │ ├── env.py
│ │ └── versions/
│ ├── app/
│ │ ├── __init__.py
│ │ ├── main.py # FastAPI 入口
│ │ ├── core/
│ │ │ ├── config.py # pydantic-settings 配置
│ │ │ └── security.py # JWT、密码哈希
│ │ ├── db/
│ │ │ ├── base.py # Declarative Base
│ │ │ └── session.py # engine、SessionLocal
│ │ ├── models/ # SQLAlchemy ORM 模型
│ │ │ └── user.py
│ │ ├── schemas/ # Pydantic 模型(请求/响应)
│ │ │ └── user.py
│ │ ├── api/ # 路由,按资源分层
│ │ │ ├── deps.py # 依赖注入(get_db, get_current_user)
│ │ │ └── v1/
│ │ │ ├── router.py
│ │ │ └── endpoints/
│ │ │ ├── auth.py
│ │ │ └── users.py
│ │ ├── crud/ # 数据库操作层(与路由分离)
│ │ │ └── user.py
│ │ └── services/ # 业务逻辑层(复杂逻辑)
│ ├── tests/
│ │ ├── conftest.py
│ │ └── test_auth.py
│ └── scripts/
│ └── start.sh # 容器启动脚本(等数据库就绪+迁移+启动)
│
├── frontend/
│ ├── Dockerfile
│ ├── .dockerignore
│ ├── package.json
│ ├── vite.config.ts
│ ├── tsconfig.json
│ ├── index.html
│ ├── nginx.conf # 生产环境静态文件服务+反向代理
│ ├── public/
│ └── src/
│ ├── main.tsx
│ ├── App.tsx
│ ├── api/ # API 客户端(axios/fetch 封装)
│ │ ├── client.ts
│ │ └── auth.ts
│ ├── hooks/
│ ├── store/ # 状态管理(zustand)
│ ├── pages/
│ ├── components/ # 通用组件
│ │ └── ui/
│ ├── types/ # TS 类型定义
│ └── styles/
│
└── docs/
├── architecture.md
└── api.md
关键分层原则 :api/(路由)→ services/(业务)→ crud/(数据访问)→ models/(表结构)。路由层不直接写 SQL,业务逻辑不混在路由函数里。
第三部分:后端搭建(FastAPI)
3.1 初始化项目与依赖管理
使用 uv(目前最快最主流的 Python 包管理器):
bash
cd backend
uv init
uv add fastapi "uvicorn[standard]" sqlalchemy asyncpg alembic pydantic-settings "python-jose[cryptography]" "passlib[bcrypt]"
uv add --dev pytest pytest-asyncio httpx ruff mypy
3.2 配置管理 app/core/config.py
python
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
model_config = SettingsConfigDict(env_file=".env", env_prefix="APP_")
PROJECT_NAME: str = "MyApp"
DATABASE_URL: str
SECRET_KEY: str
ACCESS_TOKEN_EXPIRE_MINUTES: int = 30
CORS_ORIGINS: list[str] = ["http://localhost:5173"]
DEBUG: bool = False
settings = Settings()
.env.example(入库)与 .env(不入库):
env
APP_DATABASE_URL=postgresql+asyncpg://myapp:myapp@db:5432/myapp
APP_SECRET_KEY=change-me-in-production
APP_DEBUG=true
3.3 数据库会话 app/db/session.py
python
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_session_engine # 实际为 create_async_engine
from app.core.config import settings
engine = create_async_engine(settings.DATABASE_URL, pool_pre_ping=True)
AsyncSessionLocal = async_sessionmaker(engine, expire_on_commit=False)
async def get_db() -> AsyncSession:
async with AsyncSessionLocal() as session:
yield session
3.4 ORM 模型示例 app/models/user.py
python
from datetime import datetime
from sqlalchemy import DateTime, String, func
from sqlalchemy.orm import Mapped, mapped_column
from app.db.base import Base
class User(Base):
__tablename__ = "users"
id: Mapped[int] = mapped_column(primary_key=True)
email: Mapped[str] = mapped_column(String(255), unique=True, index=True)
hashed_password: Mapped[str] = mapped_column(String(255))
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
3.5 路由示例 app/api/v1/endpoints/users.py
python
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.ext.asyncio import AsyncSession
from app.api.deps import get_db
from app.crud import user as crud_user
from app.schemas.user import UserCreate, UserRead
router = APIRouter(prefix="/users", tags=["users"])
@router.post("", response_model=UserRead, status_code=status.HTTP_201_CREATED)
async def create_user(payload: UserCreate, db: AsyncSession = Depends(get_db)):
if await crud_user.get_by_email(db, payload.email):
raise HTTPException(status_code=409, detail="Email already registered")
return await crud_user.create(db, payload)
3.6 数据库迁移(永远不要手写 SQL 建表)
bash
alembic revision --autogenerate -m "create users table"
alembic upgrade head
迁移文件必须提交到 Git,生产部署靠它升级数据库。
第四部分:前端搭建(React)
4.1 初始化
bash
cd frontend
npm create vite@latest . -- --template react-ts
npm install axios zustand react-router-dom
npm install -D tailwindcss @tanstack/react-query
4.2 API 客户端 src/api/client.ts
typescript
import axios from "axios";
export const api = axios.create({
baseURL: import.meta.env.VITE_API_URL ?? "/api/v1",
timeout: 10000,
});
api.interceptors.request.use((config) => {
const token = localStorage.getItem("token");
if (token) config.headers.Authorization = `Bearer ${token}`;
return config;
});
最佳实践 :前端请求 baseURL 用相对路径 /api/v1,由 Nginx 反向代理转发,避免 CORS 和环境切换问题。
4.3 环境变量
env
# frontend/.env.development(入库)
VITE_API_URL=/api/v1
命名必须 VITE_ 前缀,且不要放任何密钥(前端一切皆公开)。
第五部分:容器化(每个服务一个容器)
5.1 backend/Dockerfile(多阶段构建)
dockerfile
# ---------- 构建阶段 ----------
FROM python:3.12-slim AS builder
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
WORKDIR /app
COPY pyproject.toml uv.lock ./
RUN uv sync --frozen --no-dev
# ---------- 运行阶段 ----------
FROM python:3.12-slim
RUN groupadd -r appuser && useradd -r -g appuser appuser
WORKDIR /app
COPY --from=builder /app/.venv /app/.venv
COPY ./app ./app
COPY ./alembic ./alembic
COPY ./alembic.ini .
COPY ./scripts/start.sh .
RUN chmod +x start.sh
ENV PATH="/app/.venv/bin:$PATH"
USER appuser # 非 root 运行,安全最佳实践
EXPOSE 8000
CMD ["./start.sh"]
backend/scripts/start.sh:
bash
#!/bin/bash
set -e
echo "Waiting for db..."
until python -c "import socket; socket.create_connection(('${APP_DB_HOST:-db}', 5432))" 2>/dev/null; do sleep 1; done
alembic upgrade head # 启动前自动迁移
exec uvicorn app.main:app --host 0.0.0.0 --port 8000
5.2 frontend/Dockerfile(构建+nginx 两阶段)
dockerfile
FROM node:22-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM nginx:alpine
COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=builder /app/dist /usr/share/nginx/html
EXPOSE 80
frontend/nginx.conf:
nginx
server {
listen 80;
root /usr/share/nginx/html;
location / {
try_files $uri $uri/ /index.html; # SPA 路由回退
}
location /api/ {
proxy_pass http://web:8000/api/; # 反代到后端容器
proxy_set_header Host $host;
}
}
5.3 根目录 docker-compose.yml
yaml
services:
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: myapp
POSTGRES_PASSWORD: myapp
POSTGRES_DB: myapp
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U myapp"]
interval: 5s
retries: 5
web:
build: ./backend
env_file: .env
environment:
APP_DATABASE_URL: postgresql+asyncpg://myapp:myapp@db:5432/myapp
depends_on:
db:
condition: service_healthy
volumes:
- ./backend/app:/app/app # 开发热重载
ports:
- "8000:8000"
frontend:
build: ./frontend
depends_on:
- web
ports:
- "5173:80"
volumes:
pgdata:
.dockerignore(backend 和 frontend 各一份,必须配置):
.git
__pycache__/
*.pyc
.venv/
node_modules/
dist/
.env
tests/
第六部分:Git 工作流与提交规范
6.1 分支模型
main # 永远可部署,受保护分支,禁止直接 push
└── develop # 集成分支(小团队可省略)
└── feature/xxx # 每个功能一个分支
└── fix/xxx
6.2 每日工作流
bash
git checkout -b feature/user-auth # 从 main 切出
# ...编码...
git add -p # 逐块审查暂存
git commit -m "feat(auth): add login endpoint with JWT"
git push -u origin feature/user-auth
# 在 GitHub 上发起 Pull Request → CI 通过 → Code Review → Squash Merge
6.3 Conventional Commits 规范
feat: 新功能 feat(auth): add refresh token
fix: 修bug fix(db): handle connection pool exhaustion
refactor: 重构 refactor: extract crud layer
test: 测试 test: add auth endpoint tests
docs: 文档 docs: update api usage
chore: 杂务 chore: bump fastapi to 0.115
ci: CI配置 ci: add docker build workflow
6.4 .gitignore 根目录关键内容
gitignore
.env
.venv/
__pycache__/
node_modules/
dist/
*.log
.pgdata/
.idea/ .vscode/
第七部分:代码质量与测试
7.1 后端 pyproject.toml 工具配置
toml
[tool.ruff]
line-length = 100
lint.select = ["E", "F", "I", "B", "UP"]
[tool.pytest.ini_options]
asyncio_mode = "auto"
7.2 测试 backend/tests/conftest.py
python
import pytest
from httpx import ASGITransport, AsyncClient
from app.main import app
@pytest.fixture
async def client():
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as c:
yield c
测试用独立的测试数据库(或 SQLite 内存库),CI 中用 service 容器跑真实 PG。
7.3 Makefile 统一命令入口
makefile
up: ; docker compose up -d --build
down: ; docker compose down
logs: ; docker compose logs -f
migrate: ; docker compose exec web alembic upgrade head
test: ; docker compose exec web pytest -v
lint: ; cd backend && uv run ruff check . && cd ../frontend && npm run lint
第八部分:CI/CD(GitHub Actions)
.github/workflows/backend-ci.yml:
yaml
name: Backend CI
on:
pull_request:
paths: ["backend/**"]
push:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
defaults: { run: { working-directory: backend } }
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_USER: test
POSTGRES_PASSWORD: test
POSTGRES_DB: test
options: >-
--health-cmd pg_isready --health-interval 5s --health-retries 5
ports: ["5432:5432"]
env:
APP_DATABASE_URL: postgresql+asyncpg://test:test@localhost:5432/test
APP_SECRET_KEY: ci-secret
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v4
- run: uv sync
- run: uv run ruff check .
- run: uv run mypy app
- run: uv run alembic upgrade head
- run: uv run pytest -v
.github/workflows/deploy.yml(合并到 main 后自动部署):
yaml
name: Deploy
on:
push:
branches: [main]
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build & push images
run: |
echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
docker build -t ghcr.io/${{ github.repository }}/backend:latest ./backend
docker build -t ghcr.io/${{ github.repository }}/frontend:latest ./frontend
docker push ghcr.io/${{ github.repository }}/backend:latest
docker push ghcr.io/${{ github.repository }}/frontend:latest
- name: Deploy to server
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.SSH_HOST }}
username: deploy
key: ${{ secrets.SSH_KEY }}
script: |
cd /opt/myapp
docker compose -f docker-compose.prod.yml pull
docker compose -f docker-compose.prod.yml up -d --remove-orphans
所有敏感信息(SSH、密码、密钥)存 GitHub Secrets,永不写入代码。
第九部分:生产部署
9.1 docker-compose.prod.yml(生产覆盖)
yaml
services:
web:
image: ghcr.io/yourname/myapp/backend:latest # 用镜像而非 build
env_file: .env.production
restart: always
ports: [] # 不直接暴露,走反向代理
command: ["./start.sh"]
frontend:
image: ghcr.io/yourname/myapp/frontend:latest
restart: always
ports:
- "80:80"
- "443:443"
db:
restart: always
ports: [] # 数据库绝不暴露公网
9.2 服务器上(Ubuntu + 一台 VPS 即可起步)
bash
# 服务器目录:/opt/myapp,只放 compose 文件和 .env.production
sudo ufw allow 80,443/tcp && sudo ufw enable # 只开放 web 端口
# HTTPS 用 Caddy 或 certbot + nginx
生产检查清单:
- ✅ 数据库端口不暴露公网
- ✅ 容器以非 root 运行
- ✅
DEBUG=false,强密钥走环境变量 - ✅ 配置日志轮转与备份(
pg_dumpcron 定时备份) - ✅ HTTPS 强制
第十部分:完整里程碑时间线
| 阶段 | 动作 | 提交 |
|---|---|---|
| 1 | git init + README + .gitignore |
chore: init repo |
| 2 | 搭 backend 骨架 + config + db session | feat(core): project skeleton |
| 3 | User 模型 + Alembic 迁移 | feat(db): add user model and migration |
| 4 | 注册/登录 API + 测试 | feat(auth): add register and login |
| 5 | Dockerfile + docker-compose 全栈跑通 | chore: add docker compose setup |
| 6 | Vite + React 骨架 + API client | feat(frontend): init react app |
| 7 | 登录页面 + JWT 存储 + 路由守卫 | feat(frontend): add login page |
| 8 | CI workflows | ci: add backend and frontend pipelines |
| 9 | 生产 compose + 部署脚本 | feat: add production deployment |
| 10 | 上线 + 配置 HTTPS + 备份 cron | chore: production launch |
核心原则总结
- 一个容器一个进程------PG、后端、前端永远是三个容器
.env永不入库 ------用.env.example做模板- 迁移文件必须入库------数据库变更可追溯、可回滚
- main 分支受保护------一切通过 PR + CI
- 提交信息规范化------半年后你会感谢自己
- 前端走 Nginx 反代------消灭 CORS 问题
- 密钥只存在于 Secrets ------代码里永远没有密码
按这个流程走,你会得到一个开发体验好、可测试、可复现、可平滑扩展到更复杂场景 的项目。