原因
之前搞了个自动化播客,详情见字节篝火播客。因为播客本身需要处理一些 Hacker news 以及 github trending 的数据,为了保存下来后续做了个配套的网站 lumifire.io。起初为了快速上线,直接用了 vercel + supubase 的免费方案, 最近因 vercel + supubase 相继超出免费额度,所以搞了两个腾讯云轻量服务器部署应用, 一台部署 nextjs ,另一台部署 postgres 。最终确保从浏览器到 Cloudflare 以及从 Cloudflare 到你的服务器的全程流量都经过加密验证,下边会罗列大致流程以及会用到的配置文件,非傻瓜式教程,仅供参考。
supubase 迁移至自建 postgres
- 生成配置文件(可以用 https://pgtune.leopard.in.ua/ 生成配置文件),配置文件见下文, 然后启动容器。(注意:请务必使用云服务商的防火墙,仅对你的应用服务器 IP 开放 5432 端口,避免数据库暴露在公网)
services: db: image: postgres:15 restart: always env_file: - .env ports: - "5432:5432" volumes: - pgdata:/var/lib/postgresql/data - ./postgresql.conf:/etc/postgresql/postgresql.conf command: postgres -c config_file=/etc/postgresql/postgresql.conf healthcheck: test: ["CMD-SHELL", "pg_isready -U postgres -d postgres"] interval: 10s timeout: 5s retries: 5 logging: driver: "json-file" options: max-size: "10m" max-file: "3" volumes: pgdata:
# postgresql.conf listen_addresses = '*' max_connections = 100 shared_buffers = 512MB effective_cache_size = 1536MB maintenance_work_mem = 128MB checkpoint_completion_target = 0.9 wal_buffers = 16MB default_statistics_target = 100 random_page_cost = 1.1 effective_io_concurrency = 200 work_mem = 18724kB huge_pages = off min_wal_size = 1GB max_wal_size = 4GB log_destination = 'stderr' logging_collector = off log_min_duration_statement = 200 log_line_prefix = '%m [%p]: [%l-1] user=%u,db=%d,client=%h ' log_statement = 'ddl'
- 迁移数据
#!/bin/bash # !!! 重要:请填写你的 Supabase 项目信息 !!! SUPABASE_PROJECT_REF="YOUR_SUPABASE_PROJECT_REF" # 在 Supabase 项目设置的 URL 中可以找到,例如 xyz.supabase.co 中的 xyz SUPABASE_HOST="db.${SUPABASE_PROJECT_REF}.supabase.co" SUPABASE_PASSWORD="YOUR_SUPABASE_DB_PASSWORD" # 在 Supabase 项目的 Database -> Password 中找到 # 你为新数据库设置的密码 LOCAL_POSTGRES_PASSWORD="YOUR_NEW_SUPER_STRONG_PASSWORD" SCHEMAS_TO_DUMP="public" DUMP_FILE="data_dump.sql" info() { echo -e "\033[0;32m[INFO]\033[0m $1" } warn() { echo -e "\033[0;33m[WARN]\033[0m $1" } error() { echo -e "\033[0;31m[ERROR]\033[0m $1" exit 1 } set -e set -o pipefail if [ "$SUPABASE_PROJECT_REF" == "YOUR_SUPABASE_PROJECT_REF" ] || [ "$SUPABASE_PASSWORD" == "YOUR_SUPABASE_DB_PASSWORD" ] || [ "$LOCAL_POSTGRES_PASSWORD" == "YOUR_NEW_SUPER_STRONG_PASSWORD" ]; then error "请先在脚本中填写 SUPABASE_PROJECT_REF, SUPABASE_PASSWORD, 和 LOCAL_POSTGRES_PASSWORD 变量!" fi info "检查并安装 postgresql-client (包含 pg_dump 和 psql)..." if ! command -v pg_dump &> /dev/null; then sudo apt-get update && sudo apt-get install -y postgresql-client info "postgresql-client 安装完成。" else info "postgresql-client 已安装。" fi info "正在从 Supabase 数据库导出数据..." info "主机: $SUPABASE_HOST" SCHEMA_ARGS="" for s in $SCHEMAS_TO_DUMP; do SCHEMA_ARGS+="--schema=$s " done export PGPASSWORD=$SUPABASE_PASSWORD pg_dump \ --host="$SUPABASE_HOST" \ --port=5432 \ --username="postgres" \ --dbname="postgres" \ $SCHEMA_ARGS \ --no-owner \ --no-privileges \ --format=plain \ --file="$DUMP_FILE" unset PGPASSWORD if [ -f "$DUMP_FILE" ]; then info "数据成功导出到 $DUMP_FILE" else error "数据导出失败!" fi info "正在将数据导入到新的本地 PostgreSQL 实例..." export PGPASSWORD=$LOCAL_POSTGRES_PASSWORD psql \ --host=localhost \ --port=5432 \ --username=postgres \ --dbname=db \ --file="$DUMP_FILE" unset PGPASSWORD info "数据导入完成。" info "进行简单验证..." export PGPASSWORD=$LOCAL_POSTGRES_PASSWORD TABLE_COUNT=$(psql --host=localhost --port=5432 --username=postgres --dbname=db --tuples-only -c "SELECT count(*) FROM information_schema.tables WHERE table_schema = 'public';") unset PGPASSWORD info "验证完成。Public schema 中的表数量为: $(echo $TABLE_COUNT | xargs)"
部署 nextjs 应用
- nextjs 容器镜像 (
Dockerfile
) FROM node:20-alpine AS base # Install dependencies only when needed FROM base AS deps # Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed. RUN apk add --no-cache libc6-compat WORKDIR /app # Install dependencies based on the preferred package manager COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* .npmrc* ./ # Copy prisma schema for postinstall script COPY prisma ./prisma RUN \ if [ -f yarn.lock ]; then yarn --frozen-lockfile; \ elif [ -f package-lock.json ]; then npm ci; \ elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm i --frozen-lockfile; \ else echo "Lockfile not found." && exit 1; \ fi # Rebuild the source code only when needed FROM base AS builder WORKDIR /app COPY --from=deps /app/node_modules ./node_modules COPY . . # Next.js collects completely anonymous telemetry data about general usage. # Learn more here: https://nextjs.org/telemetry # Uncomment the following line in case you want to disable telemetry during the build. # ENV NEXT_TELEMETRY_DISABLED=1 RUN \ if [ -f yarn.lock ]; then yarn run build; \ elif [ -f package-lock.json ]; then npm run build; \ elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm run build; \ else echo "Lockfile not found." && exit 1; \ fi # Production image, copy all the files and run next FROM base AS runner WORKDIR /app ENV NODE_ENV=production # Uncomment the following line in case you want to disable telemetry during runtime. # ENV NEXT_TELEMETRY_DISABLED=1 RUN addgroup --system --gid 1001 nodejs RUN adduser --system --uid 1001 nextjs COPY --from=builder /app/public ./public # Automatically leverage output traces to reduce image size # https://nextjs.org/docs/advanced-features/output-file-tracing COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static USER nextjs EXPOSE 3000 ENV PORT=3000 # server.js is created by next build from the standalone output # https://nextjs.org/docs/pages/api-reference/config/next-config-js/output ENV HOSTNAME="0.0.0.0" CMD ["node", "server.js"]
- next.config.js 配置
module.exports = { output: "standalone", };
- Cloudflare 生成 Origin Certificate 并下载, 然后配置 nginx 证书
worker_processes auto;events { worker_connections 1024;}http { upstream nextjs_server { server nextjs-app:3000; } server { listen 80; server_name domain.com; # 强制跳转到 https if ($http_x_forwarded_proto != 'https') { return 301 https://$host$request_uri; } location / { proxy_pass http://nextjs_server; proxy_set_header Host $host; } } server { listen 443 ssl http2; server_name domain.com; ssl_certificate /etc/nginx/certs/domain.com.pem; ssl_certificate_key /etc/nginx/certs/domain.com.key; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384; ssl_prefer_server_ciphers off; location / { proxy_pass http://nextjs_server; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection 'upgrade'; 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_cache_bypass $http_upgrade; } location /_next/static { proxy_cache_valid 200 302 1y; proxy_pass http://nextjs_server; } }}
- 容器编排配置 (
docker-compose.yml
)services: nextjs-app: build: context: ../ dockerfile: Dockerfile image: nextjs-app:latest container_name: nextjs-app restart: always env_file: - ../.env networks: - app-network nginx: image: nginx:stable-alpine container_name: nextjs-nginx restart: always ports: - "80:80" - "443:443" volumes: - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro - ./nginx/certs:/etc/nginx/certs:ro # Cloudflare 生成的证书 depends_on: - nextjs-app networks: - app-networknetworks: app-network: driver: bridge
修改 Cloudflare DNS
- 登录你的 Cloudflare 账户,进入
domain.com
域名的 DNS 设置页面。添加或修改 A
记录,名称为 domain.com
(或 @
),内容为你部署 Next.js 应用的腾讯云服务器公网 IP 地址。确保“代理状态 (Proxy status)”为“已代理 (Proxied)”,即云朵图标为橙色。导航到 "SSL/TLS" -> "概述 (Overview)" 页面,将 SSL/TLS 加密模式设置为 "Full (Strict)"(完全-严格)。这是最安全的模式,因为它能确保从浏览器到 Cloudflare 以及从 Cloudflare 到你的服务器的全程流量都经过加密验证。