V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
爱意满满的作品展示区。
gococonut
V2EX  ›  分享创造

cloudflare + vercel + supubase 迁移至腾讯云

  •  
  •   gococonut · 93 天前 · 1955 次点击
    这是一个创建于 93 天前的主题,其中的信息可能已经有所发展或是发生改变。

    原因

    之前搞了个自动化播客,详情见字节篝火播客。因为播客本身需要处理一些 Hacker news 以及 github trending 的数据,为了保存下来后续做了个配套的网站 lumifire.io。起初为了快速上线,直接用了 vercel + supubase 的免费方案, 最近因 vercel + supubase 相继超出免费额度,所以搞了两个腾讯云轻量服务器部署应用, 一台部署 nextjs ,另一台部署 postgres 。最终确保从浏览器到 Cloudflare 以及从 Cloudflare 到你的服务器的全程流量都经过加密验证,下边会罗列大致流程以及会用到的配置文件,非傻瓜式教程,仅供参考。

    supubase 迁移至自建 postgres

    1. 生成配置文件(可以用 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'
    
    1. 迁移数据
        #!/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 应用

    1. 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"]
    
    1. next.config.js 配置
        module.exports = {
          output: "standalone",
        };
    
    1. 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;
            }
        }
    }
    
    1. 容器编排配置 (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-network
    
    networks:
      app-network:
        driver: bridge
    

    修改 Cloudflare DNS

    1. 登录你的 Cloudflare 账户,进入 domain.com 域名的 DNS 设置页面。
    2. 添加或修改 A 记录,名称为 domain.com(或 @),内容为你部署 Next.js 应用的腾讯云服务器公网 IP 地址
    3. 确保“代理状态 (Proxy status)”为“已代理 (Proxied)”,即云朵图标为橙色。
    4. 导航到 "SSL/TLS" -> "概述 (Overview)" 页面,将 SSL/TLS 加密模式设置为 "Full (Strict)"(完全-严格)。这是最安全的模式,因为它能确保从浏览器到 Cloudflare 以及从 Cloudflare 到你的服务器的全程流量都经过加密验证。
    打赏作者
    9 条回复    2025-07-06 11:03:43 +08:00
    defunct9
        1
    defunct9  
       93 天前 via iPhone
    好👍
    avenger
        2
    avenger  
       93 天前
    速度怎么样?
    迁过来要不要备案?
    gococonut
        3
    gococonut  
    OP
       93 天前 via iPhone
    @avenger 新加坡腾讯云
    gococonut
        4
    gococonut  
    OP
       93 天前 via iPhone
    @avenger 都 cloudflare 了,跟备案就没关系了😂
    ans
        5
    ans  
       92 天前
    auth 迁了没,自建好搞吗
    gococonut
        6
    gococonut  
    OP
       92 天前 via iPhone
    @ans 我用的 better auth
    fancypanda
        7
    fancypanda  
       91 天前
    其实/_next/static 这个可以不用单独配置的,cloudflare 是根据后缀名来决定是否缓存的
    gococonut
        8
    gococonut  
    OP
       91 天前 via iPhone
    @fancypanda 是的吧,你可以看下 Response Header Cf-Cache-Status 确认。
    cutchop
        9
    cutchop  
       70 天前
    为什么自建 postgres 而不是 supabase 呢? supabase 不是可以自托管吗?请指教。
    关于   ·   帮助文档   ·   自助推广系统   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   1132 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 26ms · UTC 17:30 · PVG 01:30 · LAX 10:30 · JFK 13:30
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.