独立开发者部署实战:从本地到生产的全链路指南 — 志趣 ZhiQu 返回首页独立开发者部署实战:从本地到生产的全链路指南
版权声明
本文原创发布于 zhiqu.ac,未经书面许可禁止全文转载、采集、商用;转载必须完整标注原文链接、作者。
| 阿里云轻量 | ¥24/月 | 国内 | 1 vCPU | 1GB | 3TB | 面向国内用户 |
| AWS Lightsail | $5/月 | 全球 | 1 vCPU | 1GB | 2TB | AWS 生态 |
| DigitalOcean | $6/月 | 全球 | 1 vCPU | 1GB | 1TB | 开发者友好 |
推荐组合:主力项目用 Hetzner CX22(€4.51,2C4G)+ 个人页用 Vercel 免费版。
1.2 服务器初始配置
拿到新 VPS 后,第一件事不是装 Docker —— 是加固安全。
# 1. SSH 登录
ssh root@your_server_ip
# 2. 更新系统
apt update && apt upgrade -y
# 3. 创建普通用户(不要用 root 日常操作)
adduser deployer
usermod -aG sudo deployer
# 4. 配置 SSH 密钥登录(禁用密码登录)
# 在本地机器上生成 SSH 密钥(如果还没有)
ssh-keygen -t ed25519 -C "your_email@example.com"
# 把公钥复制到服务器
ssh-copy-id -i ~/.ssh/id_ed25519.pub deployer@your_server_ip
# 5. 编辑 SSH 配置
sudo nano /etc/ssh/sshd_config
# /etc/ssh/sshd_config 关键配置
Port 2222 # 改掉默认 22 端口(减少爆破)
PermitRootLogin no # 禁用 root 登录
PasswordAuthentication no # 禁用密码登录(只能用密钥)
PubkeyAuthentication yes # 启用密钥登录
MaxAuthTries 3 # 限制尝试次数
ClientAliveInterval 300 # 5 分钟无操作断连
ClientAliveCountMax 2 # 最多 2 次心跳
# 6. 重启 SSH,新开窗口测试,确认能连再关旧窗口
sudo systemctl restart sshd
# 7. 安装防火墙
sudo apt install ufw -y
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 2222/tcp # SSH
sudo ufw allow 80/tcp # HTTP
sudo ufw allow 443/tcp # HTTPS
sudo ufw enable
# 8. 安装 fail2ban(防暴力破解)
sudo apt install fail2ban -y
# 创建 jail.local 配置
sudo cat > /etc/fail2ban/jail.local << 'EOF'
[sshd]
enabled = true
port = 2222
maxretry = 3
bantime = 3600
findtime = 600
EOF
sudo systemctl enable fail2ban --now
# 9. 设置时区和自动更新
sudo timedatectl set-timezone Asia/Shanghai
sudo apt install unattended-upgrades -y
sudo dpkg-reconfigure -plow unattended-upgrades
1.3 安装 Docker 环境
# 官方推荐的 Docker 安装方式
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh
# 把 deployer 加入 docker 组(不用 sudo 执行 docker)
sudo usermod -aG docker deployer
# 安装 Docker Compose(Docker 26+ 已内置 docker compose,但部分系统仍需独立安装)
docker compose version
# 设置 Docker 开机启动
sudo systemctl enable docker
# 验证
docker run hello-world
# 清理安装脚本
rm get-docker.sh
2. Docker Compose 多服务编排
2.1 典型全栈项目结构
my-project/
├── docker-compose.prod.yml # 生产环境
├── docker-compose.dev.yml # 开发环境
├── frontend/
│ ├── Dockerfile
│ └── ...
├── backend/
│ ├── Dockerfile
│ └── ...
├── nginx/
│ ├── nginx.conf
│ └── conf.d/
│ └── app.conf
├── scripts/
│ ├── deploy.sh # 部署脚本
│ ├── backup-db.sh # 数据库备份
│ └── restore-db.sh # 数据库恢复
└── .github/
└── workflows/
└── deploy.yml # CI/CD
2.2 完整 docker-compose.prod.yml
# docker-compose.prod.yml
# 适用于:Next.js 前端 + NestJS 后端 + PostgreSQL + Redis + Nginx
version: '3.8'
services:
# ============================
# 数据库
# ============================
postgres:
image: pgvector/pgvector:pg16
container_name: app-postgres-prod
restart: unless-stopped
environment:
POSTGRES_USER: ${DB_USER}
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_DB: ${DB_NAME}
volumes:
- pgdata:/var/lib/postgresql/data
- ./backups:/backups
ports:
- "127.0.0.1:5432:5432" # 只绑本地,不暴露公网
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DB_USER} -d ${DB_NAME}"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
networks:
- app-network
# PG 配置优化
command: |
-c shared_buffers=256MB
-c effective_cache_size=1GB
-c maintenance_work_mem=64MB
-c wal_buffers=16MB
-c max_wal_size=2GB
-c random_page_cost=1.1
-c effective_io_concurrency=200
# ============================
# 缓存
# ============================
redis:
image: redis:7-alpine
container_name: app-redis-prod
restart: unless-stopped
command: redis-server --appendonly yes --maxmemory 256mb --maxmemory-policy allkeys-lru
volumes:
- redisdata:/data
ports:
- "127.0.0.1:6379:6379"
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 5
networks:
- app-network
# ============================
# 后端 API
# ============================
backend:
build:
context: ./backend
dockerfile: Dockerfile
target: production # 多阶段构建
container_name: app-backend-prod
restart: unless-stopped
environment:
NODE_ENV: production
DATABASE_URL: postgresql://${DB_USER}:${DB_PASSWORD}@postgres:5432/${DB_NAME}
REDIS_URL: redis://redis:6379
JWT_SECRET: ${JWT_SECRET}
# ... 其他环境变量
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
networks:
- app-network
# 资源限制
deploy:
resources:
limits:
memory: 512M
cpus: '1.0'
reservations:
memory: 256M
cpus: '0.5'
# ============================
# 前端
# ============================
frontend:
build:
context: ./frontend
dockerfile: Dockerfile
target: production
args:
NEXT_PUBLIC_API_URL: https://api.yourdomain.com/api
container_name: app-frontend-prod
restart: unless-stopped
environment:
NODE_ENV: production
depends_on:
- backend
networks:
- app-network
deploy:
resources:
limits:
memory: 512M
cpus: '1.0'
# ============================
# Nginx 反向代理
# ============================
nginx:
image: nginx:alpine
container_name: app-nginx-prod
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- ./nginx/conf.d:/etc/nginx/conf.d:ro
- ./certbot/www:/var/www/certbot:ro
- ./certbot/conf:/etc/letsencrypt:ro
- nginx_logs:/var/log/nginx
depends_on:
- frontend
- backend
networks:
- app-network
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost/health || exit 1"]
interval: 30s
timeout: 5s
retries: 3
volumes:
pgdata:
driver: local
redisdata:
driver: local
nginx_logs:
driver: local
networks:
app-network:
driver: bridge
2.3 多阶段 Dockerfile
# backend/Dockerfile
# ===== 构建阶段 =====
FROM node:20-alpine AS builder
WORKDIR /app
# 利用 Docker 层缓存:先拷贝依赖文件
COPY package*.json ./
COPY prisma ./prisma/
RUN npm ci --only=production && \
cp -R node_modules node_modules_prod && \
npm ci
# 拷贝源码并构建
COPY . .
RUN npx prisma generate && \
npm run build
# ===== 生产阶段 =====
FROM node:20-alpine AS production
WORKDIR /app
# 只复制运行时需要的文件
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules_prod ./node_modules
COPY --from=builder /app/prisma ./prisma
COPY --from=builder /app/package.json ./
# 安全:非 root 用户运行
RUN addgroup -g 1001 -S appgroup && \
adduser -S appuser -u 1001 -G appgroup && \
chown -R appuser:appgroup /app
USER appuser
EXPOSE 4000
CMD ["sh", "-c", "npx prisma migrate deploy && node dist/main.js"]
# frontend/Dockerfile (Next.js)
# ===== 构建阶段 =====
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
# Next.js 构建时需要 API URL(用于 SSG/SSR)
ARG NEXT_PUBLIC_API_URL
ENV NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL
RUN npm run build
# ===== 生产阶段 =====
FROM node:20-alpine AS production
WORKDIR /app
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/public ./public
RUN addgroup -g 1001 -S appgroup && \
adduser -S appuser -u 1001 -G appgroup && \
chown -R appuser:appgroup /app
USER appuser
EXPOSE 3000
CMD ["node", "server.js"]
关键点:Next.js 需要在 next.config.js 中启用 output: 'standalone':
// next.config.js
module.exports = {
output: 'standalone', // 生成自包含的构建产物
};
3. Nginx 反向代理与 SSL
3.1 主配置文件
# nginx/nginx.conf
user nginx;
worker_processes auto;
worker_rlimit_nofile 65535;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 4096;
use epoll;
multi_accept on;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
# 日志格式(含请求时间和上游响应时间)
log_format main '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent" '
'rt=$request_time uct="$upstream_connect_time" '
'uht="$upstream_header_time" urt="$upstream_response_time"';
access_log /var/log/nginx/access.log main buffer=16k;
# 性能优化
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
# Gzip 压缩
gzip on;
gzip_comp_level 5;
gzip_min_length 256;
gzip_proxied any;
gzip_vary on;
gzip_types
text/plain
text/css
text/xml
application/json
application/javascript
application/xml+rss
image/svg+xml;
# 安全头
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;
# 限流
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=login_limit:10m rate=5r/m;
limit_conn_zone $binary_remote_addr zone=conn_limit:10m;
# 上游服务器
upstream frontend_upstream {
server frontend:3000;
keepalive 32;
}
upstream backend_upstream {
server backend:4000;
keepalive 32;
}
include /etc/nginx/conf.d/*.conf;
}
3.2 站点配置
# nginx/conf.d/app.conf
server {
listen 80;
server_name yourdomain.com www.yourdomain.com;
# Let's Encrypt 验证
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
# 强制跳转 HTTPS
location / {
return 301 https://$host$request_uri;
}
}
server {
listen 443 ssl http2;
server_name yourdomain.com www.yourdomain.com;
# SSL 证书
ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;
# SSL 安全配置(Mozilla 推荐的 Intermediate 配置)
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_stapling on;
ssl_stapling_verify on;
# 客户端最大上传大小
client_max_body_size 20M;
# ===== 前端 =====
location / {
limit_conn conn_limit 50;
proxy_pass http://frontend_upstream;
proxy_http_version 1.1;
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;
proxy_set_header Connection "";
# 静态资源缓存
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
proxy_pass http://frontend_upstream;
expires 30d;
add_header Cache-Control "public, immutable";
}
}
# ===== 后端 API =====
location /api/ {
limit_req zone=api_limit burst=20 nodelay;
limit_conn conn_limit 20;
proxy_pass http://backend_upstream;
proxy_http_version 1.1;
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;
proxy_set_header Connection "";
# 超时设置
proxy_read_timeout 60s;
proxy_send_timeout 60s;
proxy_connect_timeout 10s;
}
# ===== 登录接口特殊限流 =====
location /api/auth/login {
limit_req zone=login_limit burst=3 nodelay;
proxy_pass http://backend_upstream/api/auth/login;
proxy_http_version 1.1;
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;
}
# ===== 上传文件 =====
location /api/uploads/ {
proxy_pass http://backend_upstream;
proxy_http_version 1.1;
proxy_set_header Host $host;
client_max_body_size 20M;
}
# ===== Nginx 健康检查 =====
location /health {
return 200 "OK\n";
add_header Content-Type text/plain;
}
# ===== 禁止访问隐藏文件 =====
location ~ /\. {
deny all;
access_log off;
log_not_found off;
}
}
3.3 SSL 证书(Let's Encrypt)
#!/bin/bash
# scripts/setup-ssl.sh
DOMAIN="yourdomain.com"
EMAIL="your@email.com"
# 创建临时目录
mkdir -p ./certbot/www ./certbot/conf
# 先启动 Nginx(仅 80 端口,用于验证)
docker compose -f docker-compose.prod.yml up -d nginx
# 用 Certbot 获取证书(standalone 模式)
docker run --rm \
-v $(pwd)/certbot/conf:/etc/letsencrypt \
-v $(pwd)/certbot/www:/var/www/certbot \
certbot/certbot \
certonly --webroot \
-w /var/www/certbot \
-d $DOMAIN \
-d www.$DOMAIN \
--email $EMAIL \
--agree-tos \
--no-eff-email
# 重启 Nginx 加载 SSL
docker compose -f docker-compose.prod.yml restart nginx
echo "✅ SSL 证书获取完成!"
echo "📅 证书将在 90 天后过期,已配置自动续期"
# 添加到 crontab(每天凌晨 2:00 检查并续期)
# crontab -e
0 2 * * * cd /opt/my-project && docker run --rm -v $(pwd)/certbot/conf:/etc/letsencrypt -v $(pwd)/certbot/www:/var/www/certbot certbot/certbot renew --quiet && docker compose -f docker-compose.prod.yml restart nginx
4. 数据库部署与备份
4.1 PostgreSQL 连接安全
❌ 危险做法:
ports:
- "5432:5432" # 暴露到公网
✅ 安全做法:
ports:
- "127.0.0.1:5432:5432" # 仅本地回环
✅ 最佳做法:
# 不暴露端口,后端通过 Docker 内部网络连接
# Docker Compose 同一 network 中的服务可以通过容器名互相访问
# backend 连接:postgresql://user:pass@postgres:5432/db
4.2 自动备份脚本
#!/bin/bash
# scripts/backup-db.sh
# 数据库自动备份(建议 cron 每天凌晨 3:00 执行)
set -e
# 配置
BACKUP_DIR="/opt/my-project/backups"
RETENTION_DAYS=30 # 保留最近 30 天的备份
DB_CONTAINER="app-postgres-prod"
DB_USER="${DB_USER:-zhiqu}"
DB_NAME="${DB_NAME:-zhiqu}"
# 创建备份目录
mkdir -p "$BACKUP_DIR"
# 生成文件名
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
FILENAME="${DB_NAME}_${TIMESTAMP}.sql.gz"
FILEPATH="${BACKUP_DIR}/${FILENAME}"
echo "📦 开始备份数据库..."
# 执行 pg_dump(通过 Docker 容器)
docker exec "$DB_CONTAINER" \
pg_dump -U "$DB_USER" -d "$DB_NAME" \
--no-owner --no-acl \
| gzip > "$FILEPATH"
# 检查备份是否成功
if [ $? -eq 0 ] && [ -s "$FILEPATH" ]; then
echo "✅ 备份完成:$FILENAME ($(du -h "$FILEPATH" | cut -f1))"
else
echo "❌ 备份失败!"
exit 1
fi
# 清理旧备份
find "$BACKUP_DIR" -name "${DB_NAME}_*.sql.gz" -mtime +$RETENTION_DAYS -delete
echo "🧹 已清理 $RETENTION_DAYS 天前的旧备份"
# 可选:上传到远程存储(S3/Cloudflare R2)
if [ -n "$S3_BUCKET" ]; then
echo "☁️ 上传到远程存储..."
aws s3 cp "$FILEPATH" "s3://${S3_BUCKET}/db-backups/${FILENAME}" \
--storage-class STANDARD_IA
echo "✅ 远程备份完成"
fi
4.3 恢复备份
#!/bin/bash
# scripts/restore-db.sh
# 用法:./scripts/restore-db.sh backups/zhiqu_20260623_030000.sql.gz
set -e
BACKUP_FILE="$1"
if [ -z "$BACKUP_FILE" ]; then
echo "用法: $0 <备份文件路径>"
echo "可选备份文件:"
ls -lh backups/*.sql.gz 2>/dev/null || echo " (没有找到备份文件)"
exit 1
fi
if [ ! -f "$BACKUP_FILE" ]; then
echo "❌ 文件不存在: $BACKUP_FILE"
exit 1
fi
echo "⚠️ 即将恢复数据库,当前数据将被覆盖!"
echo " 备份文件: $BACKUP_FILE"
read -p " 确认恢复?输入 YES 继续: " CONFIRM
if [ "$CONFIRM" != "YES" ]; then
echo "已取消"
exit 0
fi
DB_CONTAINER="app-postgres-prod"
DB_USER="${DB_USER:-zhiqu}"
DB_NAME="${DB_NAME:-zhiqu}"
echo "📥 开始恢复数据库..."
# 先断开所有连接
docker exec "$DB_CONTAINER" psql -U "$DB_USER" -d postgres -c \
"SELECT pg_terminate_backend(pg_stat_activity.pid)
FROM pg_stat_activity
WHERE pg_stat_activity.datname = '$DB_NAME' AND pid <> pg_backend_pid();"
# 删除并重建数据库
docker exec "$DB_CONTAINER" psql -U "$DB_USER" -d postgres -c \
"DROP DATABASE IF EXISTS $DB_NAME;"
docker exec "$DB_CONTAINER" psql -U "$DB_USER" -d postgres -c \
"CREATE DATABASE $DB_NAME OWNER $DB_USER;"
# 恢复
gunzip -c "$BACKUP_FILE" | docker exec -i "$DB_CONTAINER" \
psql -U "$DB_USER" -d "$DB_NAME"
if [ $? -eq 0 ]; then
echo "✅ 数据库恢复完成!"
else
echo "❌ 恢复失败!"
exit 1
fi
5. CI/CD:从 git push 到自动部署
5.1 方案对比
| 方案 | 复杂度 | 费用 | GitHub 集成 | 适用 |
|---|
| GitHub → 服务器 git pull + 脚本 | 低 | 免费 | 原生 | ⭐ 独立开发者首选 |
| GitHub Actions + SSH | 中 | 免费(2000 分钟/月) | 原生 | 需要构建步骤 |
| GitHub Actions + Docker Registry | 中 | 少量费用 | 原生 | 团队协作 |
| GitLab CI + 自有 Runner | 中 | 免费 | 需额外配置 | GitLab 用户 |
| 手动部署 | 无 | 免费 | 无 | 仅原型阶段 |
5.2 方案一:服务器端 git pull(极简,推荐)
#!/bin/bash
# scripts/deploy.sh
# 放在服务器 /opt/my-project/deploy.sh
# 用法:本地 git push 后,ssh 到服务器执行此脚本
set -e
cd /opt/my-project
echo "📥 拉取最新代码..."
git pull origin main
echo "🔨 重建 Docker 镜像..."
docker compose -f docker-compose.prod.yml build --no-cache backend frontend
echo "🔄 重启服务..."
docker compose -f docker-compose.prod.yml up -d --remove-orphans
echo "🧹 清理旧镜像..."
docker image prune -af
echo "🏥 等待服务健康检查..."
sleep 5
docker compose -f docker-compose.prod.yml ps
echo "✅ 部署完成!"
5.3 方案二:GitHub Actions 自动部署
# .github/workflows/deploy.yml
name: Deploy to Production
on:
push:
branches:
- main
paths-ignore:
- 'docs/**'
- 'README.md'
- '.github/**'
workflow_dispatch: # 允许手动触发
concurrency:
group: production
cancel-in-progress: false
jobs:
test:
name: Run Tests
runs-on: ubuntu-latest
services:
postgres:
image: pgvector/pgvector:pg16
env:
POSTGRES_USER: test
POSTGRES_PASSWORD: test
POSTGRES_DB: test
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: backend/package-lock.json
- name: Install dependencies
run: cd backend && npm ci
- name: Run tests
run: cd backend && npm test
env:
DATABASE_URL: postgresql://test:test@localhost:5432/test
deploy:
name: Deploy
needs: test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Deploy to server
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.SERVER_HOST }}
port: ${{ secrets.SERVER_PORT }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SERVER_SSH_KEY }}
script: |
cd /opt/my-project
git pull origin main
docker compose -f docker-compose.prod.yml build --no-cache backend frontend
docker compose -f docker-compose.prod.yml up -d --remove-orphans
docker image prune -af
echo "✅ Deploy finished at $(date)"
- name: Health check
run: |
sleep 10
curl -f -s -o /dev/null -w "%{http_code}" https://yourdomain.com/health || exit 1
- name: Notify on failure
if: failure()
uses: slackapi/slack-github-action@v2
with:
webhook: ${{ secrets.SLACK_WEBHOOK }}
webhook-type: incoming-webhook
payload: |
{
"text": "❌ Deploy failed for ${{ github.repository }} on commit ${{ github.sha }}"
}
5.4 GitHub Secrets 配置
在 GitHub 仓库 Settings → Secrets and variables → Actions 中添加:
| Secret | 说明 |
|---|
SERVER_HOST | 服务器 IP |
SERVER_PORT | SSH 端口(如 2222) |
SERVER_USER | SSH 用户名(如 deployer) |
SERVER_SSH_KEY | 私钥内容(cat ~/.ssh/id_ed25519) |
SLACK_WEBHOOK | (可选)部署失败通知 |
6. Cloudflare CDN 与安全
6.1 为什么用 Cloudflare
对于独立开发者,Cloudflare 免费版已经足够强大:
| 功能 | 免费版 | Pro ($20/月) |
|---|
| CDN 全球加速 | ✅ | ✅ |
| DDoS 防护 | ✅(无限) | ✅ |
| 免费 SSL 证书 | ✅ | ✅ |
| WAF 防火墙规则 | 5 条 | 20 条 |
| 页面规则 | 3 条 | 20 条 |
| 图片优化 | 有限 | 更灵活 |
| Bot 管理 | 基础 | 高级 |
6.2 DNS 设置
# Cloudflare DNS 配置
Type Name Content Proxy
A @ 你的服务器 IP ✅ Proxied (橙色云朵)
A www 你的服务器 IP ✅ Proxied
CNAME api 你的服务器 IP ✅ Proxied (如果 API 在同一服务器)
6.3 关键页面规则
# Cloudflare → Rules → Page Rules
1. /api/* → Cache Level: Bypass
(API 响应不应被 CDN 缓存)
2. /uploads/* → Cache Level: Cache Everything
Edge Cache TTL: 7 days
(用户上传的文件可以长期缓存)
3. /_next/static/* → Cache Level: Cache Everything
Edge Cache TTL: 30 days
(Next.js 静态资源文件名含 hash,永不冲突)
6.4 Nginx 真实 IP 还原
Cloudflare 代理后,Nginx 看到的 IP 是 Cloudflare 的边缘节点 IP。需要还原真实用户 IP:
# nginx/nginx.conf http 块中
# Cloudflare IP 范围:https://www.cloudflare.com/ips-v4
set_real_ip_from 173.245.48.0/20;
set_real_ip_from 103.21.244.0/22;
set_real_ip_from 103.22.200.0/22;
set_real_ip_from 103.31.4.0/22;
set_real_ip_from 141.101.64.0/18;
set_real_ip_from 108.162.192.0/18;
set_real_ip_from 190.93.240.0/20;
set_real_ip_from 188.114.96.0/20;
set_real_ip_from 197.234.240.0/22;
set_real_ip_from 198.41.128.0/17;
set_real_ip_from 162.158.0.0/15;
set_real_ip_from 104.16.0.0/13;
set_real_ip_from 104.24.0.0/14;
set_real_ip_from 172.64.0.0/13;
set_real_ip_from 131.0.72.0/22;
real_ip_header CF-Connecting-IP;
6.5 WAF 规则示例
# Cloudflare → Security → WAF → Custom Rules
# 规则 1:拦截恶意 UA
(http.user_agent contains "curl") or
(http.user_agent contains "python-requests") or
(http.user_agent contains "Go-http-client")
→ Action: Block
# 规则 2:拦截 WordPress 扫描(非 WP 站点)
(http.request.uri contains "/wp-admin") or
(http.request.uri contains "/wp-login") or
(http.request.uri contains "/xmlrpc.php")
→ Action: Block
# 规则 3:API 接口频率限制
(http.request.uri.path contains "/api/" and
http.request.method eq "POST" and
rate(http.request.uri, 1 minute) > 20)
→ Action: JS Challenge
7. 安全加固清单
7.1 服务器层面
# ✅ 检查清单
# 1. SSH 安全
sudo grep -E '^PermitRootLogin|^PasswordAuthentication|^Port' /etc/ssh/sshd_config
# 预期:PermitRootLogin no, PasswordAuthentication no, Port ≠ 22
# 2. 防火墙状态
sudo ufw status verbose
# 预期:只有 80, 443, 你的 SSH 端口
# 3. 自动更新
sudo systemctl status unattended-upgrades
# 4. Fail2ban
sudo fail2ban-client status sshd
# 5. 检查监听端口
sudo ss -tlnp
# 确保数据库端口(5432, 6379)只监听 127.0.0.1
# 6. 检查非 root 进程
ps aux | grep -v '^root' | grep -E 'node|python|java'
# 确保应用以非 root 用户运行
# 7. Docker socket 权限
ls -la /var/run/docker.sock
# 确认只有 docker 组成员可以访问
7.2 应用层面
// 安全头中间件(NestJS)
import { Injectable, NestMiddleware } from '@nestjs/common';
import helmet from 'helmet';
// 在 main.ts 中使用 helmet
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'unsafe-inline'", "https://challenges.cloudflare.com"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", "data:", "https://yourdomain.com", "https://imagedelivery.net"],
connectSrc: ["'self'", "https://api.yourdomain.com"],
fontSrc: ["'self'"],
objectSrc: ["'none'"],
mediaSrc: ["'self'"],
frameSrc: ["'none'"],
},
},
crossOriginEmbedderPolicy: false,
}));
7.3 .env 安全管理
# ❌ 错误做法
.env 文件提交到 Git
环境变量包含在 docker-compose.yml 中
# ✅ 正确做法
# .gitignore
.env
.env.local
.env.prod
# 使用 .env.example 作为模板
# docker-compose 用 ${VAR} 引用环境变量
# 服务器上手动创建 .env 文件
8. 监控与告警
8.1 轻量监控方案
对于独立开发者,不需要 Prometheus + Grafana 那一套重型方案。
#!/bin/bash
# scripts/health-check.sh
# 建议每 5 分钟执行一次
WEBHOOK_URL="${DISCORD_WEBHOOK:-}" # 或 Slack webhook
APP_URL="https://yourdomain.com"
API_URL="https://yourdomain.com/api/health"
# 检查网站
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" --max-time 10 "$APP_URL")
if [ "$HTTP_CODE" != "200" ]; then
echo "❌ 网站不可用!HTTP $HTTP_CODE"
if [ -n "$WEBHOOK_URL" ]; then
curl -H "Content-Type: application/json" \
-d "{\"content\":\"🚨 网站不可用!HTTP $HTTP_CODE - $(date)\"}" \
"$WEBHOOK_URL"
fi
fi
# 检查 API
API_CODE=$(curl -s -o /dev/null -w "%{http_code}" --max-time 10 "$API_URL")
if [ "$API_CODE" != "200" ]; then
echo "❌ API 不可用!HTTP $API_CODE"
fi
# 检查磁盘空间
DISK_USAGE=$(df / | awk 'NR==2 {print $5}' | sed 's/%//')
if [ "$DISK_USAGE" -gt 85 ]; then
echo "⚠️ 磁盘使用率: ${DISK_USAGE}%"
if [ -n "$WEBHOOK_URL" ]; then
curl -H "Content-Type: application/json" \
-d "{\"content\":\"⚠️ 磁盘使用率: ${DISK_USAGE}% - $(date)\"}" \
"$WEBHOOK_URL"
fi
fi
# 检查内存
MEM_USAGE=$(free | awk 'NR==2 {printf "%.0f", $3/$2*100}')
if [ "$MEM_USAGE" -gt 90 ]; then
echo "⚠️ 内存使用率: ${MEM_USAGE}%"
fi
# 添加到 crontab
*/5 * * * * /opt/my-project/scripts/health-check.sh >> /var/log/health-check.log 2>&1
8.2 Docker 日志管理
# docker-compose.prod.yml 中为每个服务添加
services:
backend:
logging:
driver: "json-file"
options:
max-size: "50m"
max-file: "5"
# 查看服务日志
docker logs app-backend-prod --tail=50 -f
# 清理 Docker 构建缓存(定期执行)
docker builder prune -af --filter until=168h # 清理 7 天前的
docker system prune -f # 清理无用镜像和网络
9. Vercel + 后端的混合部署
9.1 什么适合放 Vercel
| 适合放 Vercel | 不适合放 Vercel |
|---|
| Next.js 前端(SSR/SSG) | WebSocket 长连接 |
| 静态页面 / 文档站 | 后台任务 / 队列 |
| Serverless API(轻量) | 数据库(需外部) |
| 边缘函数 | 耗时超过 60s 的请求 |
9.2 混合部署架构
Vercel (前端 + Serverless API)
│
│ /api/external/* 转发到后端
▼
VPS (NestJS 后端 + PostgreSQL + Redis)
│
│ API 互相调用
▼
Cloudflare (DNS + CDN + WAF)
9.3 Vercel 配置
// vercel.json (Next.js 项目根目录)
{
"rewrites": [
{
"source": "/api/external/:path*",
"destination": "https://api.yourdomain.com/api/:path*"
}
]
}
10. 故障排查手册
10.1 「Docker 容器一直重启」
# 查看退出日志
docker ps -a --filter "status=exited"
# 查看具体容器的日志
docker logs app-backend-prod --tail=50
# 常见原因
# 1. 环境变量缺失 → 检查 .env 文件
# 2. 数据库连不上 → 检查 depends_on 和 healthcheck
# 3. 端口冲突 → lsof -i :4000
# 4. 内存不足 → free -h && docker stats
10.2 「Nginx 502 Bad Gateway」
# 1. 检查上游服务是否在运行
docker ps | grep -E 'frontend|backend'
# 2. 检查 Docker 网络
docker network inspect app-network | grep -A 5 'frontend\|backend'
# 3. 检查 Nginx 能否解析容器名
docker exec app-nginx-prod ping -c 1 frontend
# 4. 检查上游健康
docker exec app-nginx-prod curl -s http://frontend:3000/health
docker exec app-nginx-prod curl -s http://backend:4000/api/health
# 5. 查看 Nginx 错误日志
docker logs app-nginx-prod --tail=30
10.3 「磁盘空间满了」
# 1. 快速定位大文件
du -sh /* 2>/dev/null | sort -rh | head -10
# 2. Docker 最占空间
docker system df
# TYPE TOTAL ACTIVE SIZE RECLAIMABLE
# Images 12 5 3.2GB 1.8GB (56%)
# Containers 8 5 150MB 0B (0%)
# Local Volumes 5 3 2.1GB 500MB (23%)
# Build Cache 20 0 4.5GB 4.5GB
# 3. 清理 Docker
docker builder prune -af # 清理构建缓存
docker image prune -af # 清理未使用的镜像
docker volume prune -f # 清理未使用的卷
# 4. 清理系统日志
sudo journalctl --vacuum-size=200M
# 5. 清理 apt 缓存
sudo apt clean
10.4 「数据库迁移失败」
# 1. 进入后端容器手动执行迁移
docker exec -it app-backend-prod sh
cd /app && npx prisma migrate status
npx prisma migrate deploy
# 2. 如果迁移卡住,手动标记
npx prisma migrate resolve --applied "migration_name"
# 3. 紧急回滚(慎用)
# 先备份数据库!见 §4.2
npx prisma migrate reset --force # 会删除所有数据!
总结
独立开发者的部署不需要过度工程化。本文覆盖的核心流程:
- 选一台 VPS(Hetzner CX22 够用大多数项目)+ 做好安全加固
- 用 Docker Compose 编排所有服务,一个
docker-compose.prod.yml 描述整个系统
- Nginx 做反向代理,统一入口,配上 Let's Encrypt SSL
- 数据库自动备份到本地 + 远程(S3/R2),脚本配 cron
- GitHub Actions 自动部署:push 代码 → 跑测试 → SSH 到服务器 → git pull + 重建
- Cloudflare 免费 CDN:加速 + DDoS 防护 + WAF
- Uptime 监控:简单的 curl + cron + webhook 就够用
> 部署的本质是让代码持续地、可靠地运行。工具越简单,出问题时越容易排查。Kubernetes 是给 100 个微服务准备的,你的项目大概率不需要。
> 本文部署架构已在生产环境验证(志趣论坛 zhiqu.ac 用的就是这套方案)。如果你用这套方案部署了自己的项目,欢迎来项目展示板块分享。