原因
之前搞了个自动化播客,详情见字节篝火播客。因为播客本身需要处理一些 Hacker news 以及 github trending 的数据,为了保存下来后续做了个配套的网站 lumifire.io。起初为了快速上线,直接用了 vercel + supubase 的免费方案, 最近因 vercel + supubase 相继超出免费额度,所以搞了两个腾讯云轻量服务器部署应用, 一台部署 nextjs,另一台部署 postgres。最终确保从浏览器到 Cloudflare 以及从 Cloudflare 到你的服务器的全程流量都经过加密验证,下边会罗列大致流程以及会用到的配置文件,非傻瓜式教程,仅供参考。
supubase 迁移至自建 postgres
- 生成配置文件(可以用 pgtune.leopard.in.ua/ 生成配置文件),配置文件见下文, 然后启动容器。(注意:请务必使用云服务商的防火墙,仅对你的应用服务器IP开放5432端口,避免数据库暴露在公网)
yml
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:
sh
# 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'
- 迁移数据
sh
#!/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
)
yml
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 配置
js
module.exports = {
output: "standalone",
};
- Cloudflare 生成 Origin Certificate 并下载, 然后配置 nginx 证书
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
)
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-network
networks:
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 到你的服务器的全程流量都经过加密验证。