diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..1a4cd88 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,15 @@ +.git +.github +.next +.contentlayer +node_modules +coverage +dist +build +test-results +*.log +.env +.env.local +.env.*.local +deploy/single-node/.env.runtime +knowledge/.git diff --git a/.env.example b/.env.example index ca08874..aed33b1 100644 --- a/.env.example +++ b/.env.example @@ -1,3 +1,26 @@ +# Frontend site base URLs +APP_BASE_URL= +NEXT_PUBLIC_APP_BASE_URL= +NEXT_PUBLIC_SITE_URL= +NEXT_PUBLIC_LOGIN_URL= +NEXT_PUBLIC_DOCS_BASE_URL= +SESSION_COOKIE_SECURE=true +NEXT_PUBLIC_SESSION_COOKIE_SECURE=true +RUNTIME_HOSTNAME= +NEXT_RUNTIME_HOSTNAME= +DEPLOYMENT_HOSTNAME= +RUNTIME_ENV=prod +REGION=cn +NEXT_PUBLIC_RUNTIME_ENVIRONMENT=prod +NEXT_PUBLIC_RUNTIME_REGION=cn + +# Upstream service endpoints +ACCOUNT_SERVICE_URL=https://accounts.svc.plus +NEXT_PUBLIC_ACCOUNT_SERVICE_URL=https://accounts.svc.plus +SERVER_SERVICE_URL=https://api.svc.plus +NEXT_PUBLIC_SERVER_SERVICE_URL=https://api.svc.plus +SERVER_SERVICE_INTERNAL_URL= + # OpenClaw assistant integrations # Use environment variables to prefill the assistant and integrations page. # Values are read server-side and are not hardcoded into the UI. @@ -23,6 +46,7 @@ INTERNAL_SERVICE_TOKEN= CLOUDFLARE_API_TOKEN= CLOUDFLARE_ACCOUNT_ID= CLOUDFLARE_WEB_ANALYTICS_SITE_TAG= +CLOUDFLARE_ZONE_TAG= # Root email whitelist for privileged user-creation actions (comma-separated) # Default: admin@svc.plus @@ -30,6 +54,7 @@ ROOT_EMAIL_WHITELIST=admin@svc.plus # Stripe public price ids used by /prices, product pages, and /panel/subscription # These values are safe to expose to the browser. Use Stripe test-mode price ids for local/dev. +NEXT_PUBLIC_PAYPAL_CLIENT_ID= NEXT_PUBLIC_STRIPE_PRICE_XSTREAM_PAYGO= NEXT_PUBLIC_STRIPE_PRICE_XSTREAM_SUBSCRIPTION= NEXT_PUBLIC_STRIPE_PRICE_XSCOPEHUB_PAYGO= diff --git a/.github/workflows/service_release_frontend-deploy.yml b/.github/workflows/service_release_frontend-deploy.yml new file mode 100644 index 0000000..c4597a5 --- /dev/null +++ b/.github/workflows/service_release_frontend-deploy.yml @@ -0,0 +1,175 @@ +name: Service Release Frontend Deploy + +on: + workflow_dispatch: + inputs: + image_tag: + description: Optional image tag override. Defaults to the current commit SHA. + required: false + type: string + push: + branches: + - main + paths: + - ".github/workflows/service_release_frontend-deploy.yml" + - "deploy/single-node/**" + - "scripts/github-actions/**" + - "src/**" + - "public/**" + - "scripts/**" + - "config/**" + - "package.json" + - "Dockerfile" + - ".env.example" + - "next.config.mjs" + - "tailwind.config.js" + - "postcss.config.mjs" + - "tsconfig.json" + - "contentlayer.config.ts" + +concurrency: + group: frontend-prod + cancel-in-progress: true + +permissions: + contents: read + packages: write + +env: + DEPLOY_HOST: 47.120.61.35 + DEPLOY_USER: root + DEPLOY_DIR: /opt/console-svc-plus + PRIMARY_DOMAIN: cn.svc.plus + SECONDARY_DOMAIN: cn.onwalk.net + +jobs: + prepare: + runs-on: ubuntu-latest + outputs: + ghcr_namespace: ${{ steps.meta.outputs.ghcr_namespace }} + image_tag: ${{ steps.meta.outputs.image_tag }} + image_ref: ${{ steps.meta.outputs.image_ref }} + steps: + - name: Compute image metadata + id: meta + shell: bash + run: | + set -euo pipefail + image_tag="${{ github.event.inputs.image_tag }}" + if [[ -z "${image_tag}" ]]; then + image_tag="${GITHUB_SHA}" + fi + ghcr_namespace="${GITHUB_REPOSITORY_OWNER,,}" + echo "ghcr_namespace=${ghcr_namespace}" >> "${GITHUB_OUTPUT}" + echo "image_tag=${image_tag}" >> "${GITHUB_OUTPUT}" + echo "image_ref=ghcr.io/${ghcr_namespace}/dashboard:${image_tag}" >> "${GITHUB_OUTPUT}" + + build: + runs-on: ubuntu-latest + needs: prepare + environment: production + steps: + - uses: actions/checkout@v4 + + - name: Clone knowledge content + run: git clone --depth=1 https://github.com/Cloud-Neutral-Workshop/knowledge.git knowledge + + - uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ github.token }} + + - uses: docker/setup-buildx-action@v3 + + - name: Build and push frontend image + uses: docker/build-push-action@v6 + with: + context: . + file: Dockerfile + platforms: linux/amd64 + push: true + tags: ${{ needs.prepare.outputs.image_ref }} + build-args: | + NODE_BUILDER_IMAGE=node:22-bookworm + NODE_RUNTIME_IMAGE=node:22-slim + CONTENTLAYER_BUILD=true + NEXT_PUBLIC_APP_BASE_URL=${{ vars.NEXT_PUBLIC_APP_BASE_URL || format('https://{0}', env.PRIMARY_DOMAIN) }} + NEXT_PUBLIC_SITE_URL=${{ vars.NEXT_PUBLIC_SITE_URL || format('https://{0}', env.PRIMARY_DOMAIN) }} + NEXT_PUBLIC_LOGIN_URL=${{ vars.NEXT_PUBLIC_LOGIN_URL || format('https://{0}/login', env.PRIMARY_DOMAIN) }} + NEXT_PUBLIC_DOCS_BASE_URL=${{ vars.NEXT_PUBLIC_DOCS_BASE_URL || format('https://{0}/docs', env.PRIMARY_DOMAIN) }} + NEXT_PUBLIC_RUNTIME_ENVIRONMENT=${{ vars.NEXT_PUBLIC_RUNTIME_ENVIRONMENT || 'prod' }} + NEXT_PUBLIC_RUNTIME_REGION=${{ vars.NEXT_PUBLIC_RUNTIME_REGION || 'cn' }} + NEXT_PUBLIC_GISCUS_REPO=${{ vars.NEXT_PUBLIC_GISCUS_REPO || 'cloud-neutral-toolkit/console.svc.plus' }} + NEXT_PUBLIC_GISCUS_REPO_ID=${{ vars.NEXT_PUBLIC_GISCUS_REPO_ID }} + NEXT_PUBLIC_GISCUS_CATEGORY=${{ vars.NEXT_PUBLIC_GISCUS_CATEGORY || 'General' }} + NEXT_PUBLIC_GISCUS_CATEGORY_ID=${{ vars.NEXT_PUBLIC_GISCUS_CATEGORY_ID }} + NEXT_PUBLIC_PAYPAL_CLIENT_ID=${{ vars.NEXT_PUBLIC_PAYPAL_CLIENT_ID }} + NEXT_PUBLIC_STRIPE_PRICE_XSTREAM_PAYGO=${{ vars.NEXT_PUBLIC_STRIPE_PRICE_XSTREAM_PAYGO }} + NEXT_PUBLIC_STRIPE_PRICE_XSTREAM_SUBSCRIPTION=${{ vars.NEXT_PUBLIC_STRIPE_PRICE_XSTREAM_SUBSCRIPTION }} + NEXT_PUBLIC_STRIPE_PRICE_XSCOPEHUB_PAYGO=${{ vars.NEXT_PUBLIC_STRIPE_PRICE_XSCOPEHUB_PAYGO }} + NEXT_PUBLIC_STRIPE_PRICE_XSCOPEHUB_SUBSCRIPTION=${{ vars.NEXT_PUBLIC_STRIPE_PRICE_XSCOPEHUB_SUBSCRIPTION }} + NEXT_PUBLIC_STRIPE_PRICE_XCLOUDFLOW_PAYGO=${{ vars.NEXT_PUBLIC_STRIPE_PRICE_XCLOUDFLOW_PAYGO }} + NEXT_PUBLIC_STRIPE_PRICE_XCLOUDFLOW_SUBSCRIPTION=${{ vars.NEXT_PUBLIC_STRIPE_PRICE_XCLOUDFLOW_SUBSCRIPTION }} + + deploy: + runs-on: ubuntu-latest + needs: + - prepare + - build + environment: production + steps: + - uses: actions/checkout@v4 + + - name: Deploy frontend stack + env: + GHCR_USERNAME: ${{ github.actor }} + GHCR_PASSWORD: ${{ github.token }} + SSH_PRIVATE_KEY: ${{ secrets.FRONTEND_DEPLOY_SSH_KEY }} + FRONTEND_IMAGE: ${{ needs.prepare.outputs.image_ref }} + APP_BASE_URL: ${{ vars.APP_BASE_URL || format('https://{0}', env.PRIMARY_DOMAIN) }} + NEXT_PUBLIC_APP_BASE_URL: ${{ vars.NEXT_PUBLIC_APP_BASE_URL || format('https://{0}', env.PRIMARY_DOMAIN) }} + NEXT_PUBLIC_SITE_URL: ${{ vars.NEXT_PUBLIC_SITE_URL || format('https://{0}', env.PRIMARY_DOMAIN) }} + NEXT_PUBLIC_LOGIN_URL: ${{ vars.NEXT_PUBLIC_LOGIN_URL || format('https://{0}/login', env.PRIMARY_DOMAIN) }} + NEXT_PUBLIC_DOCS_BASE_URL: ${{ vars.NEXT_PUBLIC_DOCS_BASE_URL || format('https://{0}/docs', env.PRIMARY_DOMAIN) }} + NEXT_PUBLIC_RUNTIME_ENVIRONMENT: ${{ vars.NEXT_PUBLIC_RUNTIME_ENVIRONMENT || 'prod' }} + NEXT_PUBLIC_RUNTIME_REGION: ${{ vars.NEXT_PUBLIC_RUNTIME_REGION || 'cn' }} + RUNTIME_HOSTNAME: ${{ vars.RUNTIME_HOSTNAME || env.PRIMARY_DOMAIN }} + NEXT_RUNTIME_HOSTNAME: ${{ vars.NEXT_RUNTIME_HOSTNAME || env.PRIMARY_DOMAIN }} + DEPLOYMENT_HOSTNAME: ${{ vars.DEPLOYMENT_HOSTNAME || env.PRIMARY_DOMAIN }} + ACCOUNT_SERVICE_URL: ${{ vars.ACCOUNT_SERVICE_URL || 'https://accounts.svc.plus' }} + NEXT_PUBLIC_ACCOUNT_SERVICE_URL: ${{ vars.NEXT_PUBLIC_ACCOUNT_SERVICE_URL || vars.ACCOUNT_SERVICE_URL || 'https://accounts.svc.plus' }} + SERVER_SERVICE_URL: ${{ vars.SERVER_SERVICE_URL || 'https://api.svc.plus' }} + NEXT_PUBLIC_SERVER_SERVICE_URL: ${{ vars.NEXT_PUBLIC_SERVER_SERVICE_URL || vars.SERVER_SERVICE_URL || 'https://api.svc.plus' }} + SERVER_SERVICE_INTERNAL_URL: ${{ vars.SERVER_SERVICE_INTERNAL_URL }} + ROOT_EMAIL_WHITELIST: ${{ vars.ROOT_EMAIL_WHITELIST || 'admin@svc.plus' }} + OPENCLAW_GATEWAY_REMOTE_URL: ${{ vars.OPENCLAW_GATEWAY_REMOTE_URL }} + OPENCLAW_GATEWAY_TOKEN: ${{ secrets.OPENCLAW_GATEWAY_TOKEN }} + VAULT_SERVER_URL: ${{ vars.VAULT_SERVER_URL }} + VAULT_NAMESPACE: ${{ vars.VAULT_NAMESPACE }} + VAULT_TOKEN: ${{ secrets.VAULT_TOKEN }} + APISIX_AI_GATEWAY_URL: ${{ vars.APISIX_AI_GATEWAY_URL }} + AI_GATEWAY_ACCESS_TOKEN: ${{ secrets.AI_GATEWAY_ACCESS_TOKEN }} + INTERNAL_SERVICE_TOKEN: ${{ secrets.INTERNAL_SERVICE_TOKEN }} + CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} + CLOUDFLARE_ACCOUNT_ID: ${{ vars.CLOUDFLARE_ACCOUNT_ID }} + CLOUDFLARE_WEB_ANALYTICS_SITE_TAG: ${{ vars.CLOUDFLARE_WEB_ANALYTICS_SITE_TAG }} + CLOUDFLARE_ZONE_TAG: ${{ vars.CLOUDFLARE_ZONE_TAG }} + NEXT_PUBLIC_GISCUS_REPO: ${{ vars.NEXT_PUBLIC_GISCUS_REPO || 'cloud-neutral-toolkit/console.svc.plus' }} + NEXT_PUBLIC_GISCUS_REPO_ID: ${{ vars.NEXT_PUBLIC_GISCUS_REPO_ID }} + NEXT_PUBLIC_GISCUS_CATEGORY: ${{ vars.NEXT_PUBLIC_GISCUS_CATEGORY || 'General' }} + NEXT_PUBLIC_GISCUS_CATEGORY_ID: ${{ vars.NEXT_PUBLIC_GISCUS_CATEGORY_ID }} + NEXT_PUBLIC_PAYPAL_CLIENT_ID: ${{ vars.NEXT_PUBLIC_PAYPAL_CLIENT_ID }} + NEXT_PUBLIC_STRIPE_PRICE_XSTREAM_PAYGO: ${{ vars.NEXT_PUBLIC_STRIPE_PRICE_XSTREAM_PAYGO }} + NEXT_PUBLIC_STRIPE_PRICE_XSTREAM_SUBSCRIPTION: ${{ vars.NEXT_PUBLIC_STRIPE_PRICE_XSTREAM_SUBSCRIPTION }} + NEXT_PUBLIC_STRIPE_PRICE_XSCOPEHUB_PAYGO: ${{ vars.NEXT_PUBLIC_STRIPE_PRICE_XSCOPEHUB_PAYGO }} + NEXT_PUBLIC_STRIPE_PRICE_XSCOPEHUB_SUBSCRIPTION: ${{ vars.NEXT_PUBLIC_STRIPE_PRICE_XSCOPEHUB_SUBSCRIPTION }} + NEXT_PUBLIC_STRIPE_PRICE_XCLOUDFLOW_PAYGO: ${{ vars.NEXT_PUBLIC_STRIPE_PRICE_XCLOUDFLOW_PAYGO }} + NEXT_PUBLIC_STRIPE_PRICE_XCLOUDFLOW_SUBSCRIPTION: ${{ vars.NEXT_PUBLIC_STRIPE_PRICE_XCLOUDFLOW_SUBSCRIPTION }} + run: bash scripts/github-actions/deploy-frontend-single-node.sh + + - name: Verify primary domain + run: curl -fsSIL "https://${PRIMARY_DOMAIN}" + + - name: Verify secondary domain redirect + run: curl -fsSIL "https://${SECONDARY_DOMAIN}" diff --git a/.gitignore b/.gitignore index 5e10ad2..12a6776 100644 --- a/.gitignore +++ b/.gitignore @@ -55,6 +55,7 @@ coverage/ .env .env.local .env.*.local +deploy/single-node/.env.runtime # Build artifacts build/ diff --git a/Dockerfile b/Dockerfile index 26b2c45..750b06b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,6 +4,23 @@ ARG NODE_BUILDER_IMAGE=node:22-bookworm ARG NODE_RUNTIME_IMAGE=node:22-slim ARG CONTENTLAYER_BUILD=true +ARG NEXT_PUBLIC_APP_BASE_URL= +ARG NEXT_PUBLIC_SITE_URL= +ARG NEXT_PUBLIC_LOGIN_URL= +ARG NEXT_PUBLIC_DOCS_BASE_URL= +ARG NEXT_PUBLIC_RUNTIME_ENVIRONMENT= +ARG NEXT_PUBLIC_RUNTIME_REGION= +ARG NEXT_PUBLIC_GISCUS_REPO= +ARG NEXT_PUBLIC_GISCUS_REPO_ID= +ARG NEXT_PUBLIC_GISCUS_CATEGORY= +ARG NEXT_PUBLIC_GISCUS_CATEGORY_ID= +ARG NEXT_PUBLIC_PAYPAL_CLIENT_ID= +ARG NEXT_PUBLIC_STRIPE_PRICE_XSTREAM_PAYGO= +ARG NEXT_PUBLIC_STRIPE_PRICE_XSTREAM_SUBSCRIPTION= +ARG NEXT_PUBLIC_STRIPE_PRICE_XSCOPEHUB_PAYGO= +ARG NEXT_PUBLIC_STRIPE_PRICE_XSCOPEHUB_SUBSCRIPTION= +ARG NEXT_PUBLIC_STRIPE_PRICE_XCLOUDFLOW_PAYGO= +ARG NEXT_PUBLIC_STRIPE_PRICE_XCLOUDFLOW_SUBSCRIPTION= # ------------------------------------------------------- # Stage 1 — Builder (Turbopack + standalone) @@ -12,8 +29,43 @@ FROM ${NODE_BUILDER_IMAGE} AS builder WORKDIR /app/dashboard +ARG NEXT_PUBLIC_APP_BASE_URL +ARG NEXT_PUBLIC_SITE_URL +ARG NEXT_PUBLIC_LOGIN_URL +ARG NEXT_PUBLIC_DOCS_BASE_URL +ARG NEXT_PUBLIC_RUNTIME_ENVIRONMENT +ARG NEXT_PUBLIC_RUNTIME_REGION +ARG NEXT_PUBLIC_GISCUS_REPO +ARG NEXT_PUBLIC_GISCUS_REPO_ID +ARG NEXT_PUBLIC_GISCUS_CATEGORY +ARG NEXT_PUBLIC_GISCUS_CATEGORY_ID +ARG NEXT_PUBLIC_PAYPAL_CLIENT_ID +ARG NEXT_PUBLIC_STRIPE_PRICE_XSTREAM_PAYGO +ARG NEXT_PUBLIC_STRIPE_PRICE_XSTREAM_SUBSCRIPTION +ARG NEXT_PUBLIC_STRIPE_PRICE_XSCOPEHUB_PAYGO +ARG NEXT_PUBLIC_STRIPE_PRICE_XSCOPEHUB_SUBSCRIPTION +ARG NEXT_PUBLIC_STRIPE_PRICE_XCLOUDFLOW_PAYGO +ARG NEXT_PUBLIC_STRIPE_PRICE_XCLOUDFLOW_SUBSCRIPTION + ENV NEXT_TELEMETRY_DISABLED=1 \ - NEXT_PRIVATE_TURBOPACK=1 + NEXT_PRIVATE_TURBOPACK=1 \ + NEXT_PUBLIC_APP_BASE_URL=${NEXT_PUBLIC_APP_BASE_URL} \ + NEXT_PUBLIC_SITE_URL=${NEXT_PUBLIC_SITE_URL} \ + NEXT_PUBLIC_LOGIN_URL=${NEXT_PUBLIC_LOGIN_URL} \ + NEXT_PUBLIC_DOCS_BASE_URL=${NEXT_PUBLIC_DOCS_BASE_URL} \ + NEXT_PUBLIC_RUNTIME_ENVIRONMENT=${NEXT_PUBLIC_RUNTIME_ENVIRONMENT} \ + NEXT_PUBLIC_RUNTIME_REGION=${NEXT_PUBLIC_RUNTIME_REGION} \ + NEXT_PUBLIC_GISCUS_REPO=${NEXT_PUBLIC_GISCUS_REPO} \ + NEXT_PUBLIC_GISCUS_REPO_ID=${NEXT_PUBLIC_GISCUS_REPO_ID} \ + NEXT_PUBLIC_GISCUS_CATEGORY=${NEXT_PUBLIC_GISCUS_CATEGORY} \ + NEXT_PUBLIC_GISCUS_CATEGORY_ID=${NEXT_PUBLIC_GISCUS_CATEGORY_ID} \ + NEXT_PUBLIC_PAYPAL_CLIENT_ID=${NEXT_PUBLIC_PAYPAL_CLIENT_ID} \ + NEXT_PUBLIC_STRIPE_PRICE_XSTREAM_PAYGO=${NEXT_PUBLIC_STRIPE_PRICE_XSTREAM_PAYGO} \ + NEXT_PUBLIC_STRIPE_PRICE_XSTREAM_SUBSCRIPTION=${NEXT_PUBLIC_STRIPE_PRICE_XSTREAM_SUBSCRIPTION} \ + NEXT_PUBLIC_STRIPE_PRICE_XSCOPEHUB_PAYGO=${NEXT_PUBLIC_STRIPE_PRICE_XSCOPEHUB_PAYGO} \ + NEXT_PUBLIC_STRIPE_PRICE_XSCOPEHUB_SUBSCRIPTION=${NEXT_PUBLIC_STRIPE_PRICE_XSCOPEHUB_SUBSCRIPTION} \ + NEXT_PUBLIC_STRIPE_PRICE_XCLOUDFLOW_PAYGO=${NEXT_PUBLIC_STRIPE_PRICE_XCLOUDFLOW_PAYGO} \ + NEXT_PUBLIC_STRIPE_PRICE_XCLOUDFLOW_SUBSCRIPTION=${NEXT_PUBLIC_STRIPE_PRICE_XCLOUDFLOW_SUBSCRIPTION} # --------------------------- # 基础镜像升级到最新 @@ -59,6 +111,7 @@ RUN apt-get update \ COPY --from=builder /app/dashboard/.next/standalone ./ COPY --from=builder /app/dashboard/.next/static ./static COPY --from=builder /app/dashboard/public ./public +COPY --from=builder /app/dashboard/knowledge ./knowledge COPY --from=builder /app/dashboard/src/content/blog ./src/content/blog # --------------------------- diff --git a/deploy/single-node/.env.runtime.example b/deploy/single-node/.env.runtime.example new file mode 100644 index 0000000..0526d49 --- /dev/null +++ b/deploy/single-node/.env.runtime.example @@ -0,0 +1,54 @@ +# Compose settings +FRONTEND_IMAGE=ghcr.io/cloud-neutral-toolkit/dashboard:replace-me +PRIMARY_DOMAIN=cn.svc.plus +SECONDARY_DOMAIN=cn.onwalk.net + +# Frontend runtime +NODE_ENV=production +PORT=3000 +RUNTIME_ENV=prod +REGION=cn +APP_BASE_URL=https://cn.svc.plus +NEXT_PUBLIC_APP_BASE_URL=https://cn.svc.plus +NEXT_PUBLIC_SITE_URL=https://cn.svc.plus +NEXT_PUBLIC_LOGIN_URL=https://cn.svc.plus/login +NEXT_PUBLIC_DOCS_BASE_URL=https://cn.svc.plus/docs +SESSION_COOKIE_SECURE=true +NEXT_PUBLIC_SESSION_COOKIE_SECURE=true +RUNTIME_HOSTNAME=cn.svc.plus +DEPLOYMENT_HOSTNAME=cn.svc.plus +NEXT_PUBLIC_RUNTIME_ENVIRONMENT=prod +NEXT_PUBLIC_RUNTIME_REGION=cn + +# Upstream service URLs +ACCOUNT_SERVICE_URL=https://accounts.svc.plus +NEXT_PUBLIC_ACCOUNT_SERVICE_URL=https://accounts.svc.plus +SERVER_SERVICE_URL=https://api.svc.plus +NEXT_PUBLIC_SERVER_SERVICE_URL=https://api.svc.plus +SERVER_SERVICE_INTERNAL_URL= + +# Optional integrations +OPENCLAW_GATEWAY_REMOTE_URL= +OPENCLAW_GATEWAY_TOKEN= +VAULT_SERVER_URL= +VAULT_NAMESPACE= +VAULT_TOKEN= +APISIX_AI_GATEWAY_URL= +AI_GATEWAY_ACCESS_TOKEN= +INTERNAL_SERVICE_TOKEN= +CLOUDFLARE_API_TOKEN= +CLOUDFLARE_ACCOUNT_ID= +CLOUDFLARE_WEB_ANALYTICS_SITE_TAG= +CLOUDFLARE_ZONE_TAG= +ROOT_EMAIL_WHITELIST=admin@svc.plus +NEXT_PUBLIC_PAYPAL_CLIENT_ID= +NEXT_PUBLIC_GISCUS_REPO=cloud-neutral-toolkit/console.svc.plus +NEXT_PUBLIC_GISCUS_REPO_ID= +NEXT_PUBLIC_GISCUS_CATEGORY=General +NEXT_PUBLIC_GISCUS_CATEGORY_ID= +NEXT_PUBLIC_STRIPE_PRICE_XSTREAM_PAYGO= +NEXT_PUBLIC_STRIPE_PRICE_XSTREAM_SUBSCRIPTION= +NEXT_PUBLIC_STRIPE_PRICE_XSCOPEHUB_PAYGO= +NEXT_PUBLIC_STRIPE_PRICE_XSCOPEHUB_SUBSCRIPTION= +NEXT_PUBLIC_STRIPE_PRICE_XCLOUDFLOW_PAYGO= +NEXT_PUBLIC_STRIPE_PRICE_XCLOUDFLOW_SUBSCRIPTION= diff --git a/deploy/single-node/Caddyfile b/deploy/single-node/Caddyfile new file mode 100644 index 0000000..cc58ba9 --- /dev/null +++ b/deploy/single-node/Caddyfile @@ -0,0 +1,32 @@ +{$PRIMARY_DOMAIN}, {$SECONDARY_DOMAIN} { + encode zstd gzip + + @secondary host {$SECONDARY_DOMAIN} + redir @secondary https://{$PRIMARY_DOMAIN}{uri} permanent + + @next_static path /_next/static/* + handle @next_static { + root * /srv + header Cache-Control "public, max-age=31536000, immutable" + file_server + } + + @public_assets { + file { + root /srv/public + try_files {path} + } + } + handle @public_assets { + root * /srv/public + header Cache-Control "public, max-age=3600" + file_server + } + + reverse_proxy dashboard:3000 { + header_up Host {host} + header_up X-Forwarded-Host {host} + header_up X-Forwarded-Proto {scheme} + header_up X-Forwarded-For {remote_host} + } +} diff --git a/deploy/single-node/docker-compose.yml b/deploy/single-node/docker-compose.yml new file mode 100644 index 0000000..a46fed7 --- /dev/null +++ b/deploy/single-node/docker-compose.yml @@ -0,0 +1,54 @@ +services: + frontend-assets: + image: ${FRONTEND_IMAGE:?set FRONTEND_IMAGE in .env.runtime} + restart: "no" + command: + - /bin/sh + - -c + - | + set -eu + rm -rf /assets/_next /assets/public + mkdir -p /assets/_next /assets/public + cp -R /app/dashboard/static /assets/_next/static + cp -R /app/dashboard/public/. /assets/public + volumes: + - frontend_static:/assets + + dashboard: + image: ${FRONTEND_IMAGE:?set FRONTEND_IMAGE in .env.runtime} + restart: unless-stopped + env_file: + - .env.runtime + environment: + NODE_ENV: production + PORT: 3000 + networks: + - frontend + + caddy: + image: caddy:2.10-alpine + restart: unless-stopped + depends_on: + - dashboard + ports: + - "80:80" + - "443:443" + environment: + PRIMARY_DOMAIN: ${PRIMARY_DOMAIN:?set PRIMARY_DOMAIN in .env.runtime} + SECONDARY_DOMAIN: ${SECONDARY_DOMAIN:?set SECONDARY_DOMAIN in .env.runtime} + volumes: + - ./Caddyfile:/etc/caddy/Caddyfile:ro + - frontend_static:/srv:ro + - caddy_data:/data + - caddy_config:/config + networks: + - frontend + +networks: + frontend: + driver: bridge + +volumes: + frontend_static: + caddy_data: + caddy_config: diff --git a/docs/en/deployment.md b/docs/en/deployment.md index daf6f0a..0e1a979 100644 --- a/docs/en/deployment.md +++ b/docs/en/deployment.md @@ -1,31 +1,28 @@ # Deployment -This repository primarily delivers a web frontend experience and should document product flows, UI boundaries, and integration touchpoints. +## Production Baseline -Use this page to standardize deployment prerequisites, supported topologies, operational checks, and rollback notes. +- Runtime: `Caddy + Docker Compose` +- Deploy host: `47.120.61.35` +- Domains: + - `cn.svc.plus` + - `cn.onwalk.net` +- Frontend release workflow: `.github/workflows/service_release_frontend-deploy.yml` -## Current code-aligned notes +## Operating Model -- Documentation target: `console.svc.plus` -- Repo kind: `frontend` -- Manifest and build evidence: package.json (`dashboard`) -- Primary implementation and ops directories: `src/`, `scripts/`, `tests/`, `config/`, `public/` -- Package scripts snapshot: `dev`, `prebuild`, `build`, `build:static`, `start`, `lint` +The frontend is built in GitHub Actions and shipped as a prebuilt `linux/amd64` image. The host only pulls the image and starts containers; it does not build locally. -## Existing docs to reconcile +The stack is static-first: + +- Caddy serves `/_next/static/*` and public assets from a shared volume. +- The Next.js standalone container serves dynamic HTML, auth endpoints, and API proxy routes. +- `knowledge/` is cloned in CI and packed into the image during the Docker build. + +This baseline is intentional for the current weak-IO single-node host. If `docs.svc.plus` becomes an API-backed service later, update this page and the runbook to remove docs payload from the frontend image. + +## Related Docs -- `development/dev-setup.md` -- `getting-started/installation.md` -- `getting-started/quickstart.md` -- `governance/release-process.md` -- `operations/runbooks/README.md` -- `operations/runbooks/rag-server.md` - `usage/deployment.md` -- `zh/development/dev-setup.md` - -## What this page should cover next - -- Describe the current implementation rather than an aspirational future-only design. -- Keep terminology aligned with the repository root README, manifests, and actual directories. -- Link deeper runbooks, specs, or subsystem notes from the legacy docs listed above. -- Verify deployment steps against current scripts, manifests, CI/CD flow, and environment contracts before each release. +- `governance/release-process.md` +- `development/dev-setup.md` diff --git a/docs/plans/2026-03-18-frontend-single-node-deploy.md b/docs/plans/2026-03-18-frontend-single-node-deploy.md new file mode 100644 index 0000000..781973b --- /dev/null +++ b/docs/plans/2026-03-18-frontend-single-node-deploy.md @@ -0,0 +1,281 @@ +# Console Frontend Single-Node Deployment Design + +## Scope + +- Repository: `console.svc.plus` +- Target host: `root@47.120.61.35` +- Public domains: + - `cn.svc.plus` + - `cn.onwalk.net` +- Delivery mode: `GitHub Actions + GHCR + Caddy + Docker Compose` + +This document defines the deployment baseline for the China-facing frontend node. The source of truth is this upstream repository. The control-plane repository may consume the repo through git submodule, but should not become the primary place where this deployment design lives. + +## Objective + +Provide an independent frontend deployment pipeline for `console.svc.plus` that fits the current host constraints: + +- the host IO is weak +- the host must not build Docker images locally +- the frontend should run in a static-first mode where possible +- deployment logic should stay in checked-in scripts, not be embedded in GitHub Actions YAML + +The result should support repeatable releases, quick rollback by image tag, and minimal work on the target machine. + +## Constraints + +### Host constraints + +- `47.120.61.35` is a single-node host +- deployment user is `root` +- local image build on the host is explicitly disallowed +- IO pressure should be minimized during release + +### Application constraints + +- `console.svc.plus` is not a purely static site +- auth routes, same-origin API proxy routes, and selected dynamic pages still require a running Next.js server +- some `NEXT_PUBLIC_*` variables are compiled into the frontend bundle at image build time +- `prebuild` pulls documentation and `knowledge` content, so CI must prepare those inputs before building the image + +### Repository constraints + +- workflow YAML should remain orchestration-only +- service-local operational notes should remain in this repo +- downstream control repos can reference this repo through submodule updates after upstream changes are pushed + +## Recommended Topology + +### 1. CI build on GitHub Actions + +The workflow builds a single `linux/amd64` image in GitHub Actions and pushes it to GHCR. + +Reasons: + +- matches the target host architecture +- avoids multi-arch overhead for this single-node release path +- avoids local host build IO and CPU pressure +- keeps release artifacts immutable and rollback-friendly + +### 2. Runtime on the host + +Use `docker compose` with three services: + +- `dashboard`: Next.js standalone runtime +- `frontend-assets`: one-shot container that copies static files from the image into a Docker volume +- `caddy`: TLS termination, redirect handling, static file serving, and reverse proxy + +This keeps the host work limited to: + +- image pull +- asset extraction from the image +- container restart + +### 3. Static-first request flow + +Caddy serves: + +- `/_next/static/*` +- checked-in `public/` assets + +Next.js serves: + +- HTML responses +- `/api/*` routes +- auth/session flows +- dynamic pages that still depend on server runtime + +This reduces repeat disk reads and network hops for the bulk of frontend traffic while preserving the dynamic behavior the app still needs. + +## Build-Time vs Runtime Configuration + +### Build-time config + +These values must be available during Docker build because the frontend bundle reads them directly: + +- `NEXT_PUBLIC_APP_BASE_URL` +- `NEXT_PUBLIC_SITE_URL` +- `NEXT_PUBLIC_LOGIN_URL` +- `NEXT_PUBLIC_DOCS_BASE_URL` +- `NEXT_PUBLIC_RUNTIME_ENVIRONMENT` +- `NEXT_PUBLIC_RUNTIME_REGION` +- `NEXT_PUBLIC_GISCUS_*` +- `NEXT_PUBLIC_PAYPAL_CLIENT_ID` +- `NEXT_PUBLIC_STRIPE_*` + +These are injected in GitHub Actions as Docker build args. + +### Runtime config + +These values are rendered into `.env.runtime` and copied to the host: + +- upstream service URLs such as `ACCOUNT_SERVICE_URL` +- tokens used only on the server side +- Cloudflare analytics credentials +- internal service token +- runtime hostname hints + +This separation avoids rebuilding for purely server-side secret or endpoint changes when the public frontend bundle does not change. + +## Knowledge and Docs Handling + +Current decision: + +- `knowledge/` is cloned during CI +- the cloned content is included in the image build context +- the built image contains the resulting content needed by the current frontend + +Reason: + +- `prebuild` depends on this material +- the host should not fetch or generate content during deployment + +Temporary nature: + +- today the frontend still carries docs-related payload +- later, when `docs.svc.plus` becomes an API/service, docs delivery should move out of the frontend image +- that future change should reduce image size and simplify the runtime responsibilities of `console.svc.plus` + +## Domain Handling + +Primary domain: + +- `cn.svc.plus` + +Secondary domain: + +- `cn.onwalk.net` + +Current routing decision: + +- Caddy accepts both domains +- requests for `cn.onwalk.net` are redirected permanently to `cn.svc.plus` + +Reason: + +- avoid duplicate canonical origins +- keep cookie and login behavior centered on one primary host +- simplify SEO and observability interpretation + +## Release Workflow + +### Trigger + +Independent workflow: + +- `.github/workflows/service_release_frontend-deploy.yml` + +### Steps + +1. check out repository +2. clone `knowledge` +3. build and push `ghcr.io//dashboard:` +4. render `.env.runtime` +5. upload compose/caddy/env files to the host +6. log in to GHCR on the host +7. pull the new image +8. run `frontend-assets` +9. start or refresh `dashboard` and `caddy` +10. verify both domains + +### Why separate from the existing image workflow + +The existing image workflow is broader and oriented toward generic image publishing. This single-node frontend workflow needs tighter control over: + +- build-time public env injection +- production deployment sequencing +- SSH-based single-host rollout +- host-specific runtime file rendering + +So the frontend release path should remain explicit and independent. + +## Rollback Model + +Rollback unit: + +- image tag reference in `.env.runtime` + +Rollback steps: + +1. set `FRONTEND_IMAGE` to a previous known-good tag +2. rerun `frontend-assets` +3. restart `dashboard` and `caddy` +4. verify `cn.svc.plus` + +This avoids rebuilding and keeps rollback cheap on the weak-IO host. + +## Security and Secret Handling + +Secrets must not be committed to the repo. The workflow should consume: + +- `FRONTEND_DEPLOY_SSH_KEY` +- service tokens +- vault tokens +- internal service token +- optional Cloudflare credentials + +Public defaults and non-secret values belong in checked-in examples or GitHub repository/environment variables. Secret-only values stay in GitHub Secrets and are rendered into the host runtime env during deployment. + +## Operational Risks + +### Risk 1: build-time public env mismatch + +If GitHub environment variables are incomplete, the image may build successfully but the frontend can render wrong links or lose third-party integration IDs. + +Mitigation: + +- keep `.env.example` aligned +- document required GitHub `vars` +- keep the build args list explicit + +### Risk 2: image layout drift + +If the Docker image no longer contains `/app/dashboard/static` or `/app/dashboard/public`, the `frontend-assets` step fails. + +Mitigation: + +- keep asset extraction paths documented +- update deploy scripts whenever Dockerfile output layout changes + +### Risk 3: docs payload growth + +Bundling docs and `knowledge` into the frontend image increases image size. + +Mitigation: + +- accept it temporarily +- revisit once `docs.svc.plus` is externalized + +### Risk 4: single-node blast radius + +The host handles both reverse proxy and app runtime. Misconfiguration affects the whole frontend surface. + +Mitigation: + +- keep compose simple +- keep Caddy config minimal +- use image-tag rollback + +## Future Follow-Up + +### Near term + +- populate required GitHub `vars` and `secrets` +- run the workflow against `47.120.61.35` +- validate DNS, TLS, static assets, login flow, and upstream API proxy behavior + +### Later + +- move docs delivery out of the frontend image after `docs.svc.plus` is service/API based +- consider splitting static assets to object storage or CDN if traffic grows +- evaluate whether the host should keep only Caddy plus one app container, or whether docs can be removed entirely from this runtime + +## Source of Truth Rule + +For this deployment design: + +- upstream repo source of truth: `console.svc.plus` +- service-local design note location: `docs/plans/` +- control-plane repo role: consume via git submodule after upstream commit is pushed + +Do not move the primary design ownership to the control-plane repository. diff --git a/docs/plans/README.md b/docs/plans/README.md new file mode 100644 index 0000000..94821ad --- /dev/null +++ b/docs/plans/README.md @@ -0,0 +1,5 @@ +# Plans + +This directory stores service-local design notes and implementation plans for `console.svc.plus`. + +The source of truth stays in this upstream repository. Control-plane repositories may reference these documents through git submodule updates after upstream changes are pushed. diff --git a/docs/usage/deployment.md b/docs/usage/deployment.md index 8a11d05..173e84e 100644 --- a/docs/usage/deployment.md +++ b/docs/usage/deployment.md @@ -3,146 +3,142 @@ ## Scope - Runtime: `console.svc.plus` -- Frontend host: Vercel -- Edge: Cloudflare -- Auth backend: `https://accounts.svc.plus` +- Topology: `Caddy + Docker Compose + GitHub Actions` +- Deploy host: `root@47.120.61.35` +- Public domains: + - `https://cn.svc.plus` + - `https://cn.onwalk.net` +- Primary origin: `https://cn.svc.plus` -This runbook is the minimum checklist for production incidents where login or MFA stops working and browser devtools show `/api/auth/login` or `/api/auth/mfa/*` failures. +## Current Delivery Model -## Expected Request Flow +The production frontend is deployed as a prebuilt container image from GitHub Actions. -1. Browser loads `https://console.svc.plus/login` -2. Browser calls same-origin Next routes on `console.svc.plus` -3. Next route proxies server-side to `https://accounts.svc.plus/api/auth/*` -4. `accounts.svc.plus` returns either a session token or an MFA challenge +- The target host does not build images locally. +- The workflow builds an `linux/amd64` image and pushes it to `ghcr.io//dashboard:`. +- The host only performs `docker login`, `docker compose pull`, static asset extraction, and `docker compose up`. +- `knowledge/` is cloned during CI build and packed into the image. +- Static assets are extracted from the image into a shared Docker volume so Caddy can serve `/_next/static/*` and checked-in public files directly. -The browser should not call `accounts.svc.plus` directly for login. +This is intentionally static-first for the current weak-IO single-node host. Dynamic HTML, auth routes, and API proxy routes still run through the Next.js container. When `docs.svc.plus` is later split into an API/service, revisit this runbook and remove docs content from the frontend image. -## Fast Triage +## Runtime Layout -Run these checks first: +Remote directory: ```bash -curl -si https://console.svc.plus/login | sed -n '1,20p' -curl -si https://console.svc.plus/api/auth/login | sed -n '1,20p' -curl -si https://accounts.svc.plus/healthz | sed -n '1,20p' -curl -si https://accounts.svc.plus/api/auth/login | sed -n '1,20p' +/opt/console-svc-plus ``` -Interpretation: +Files deployed there: -- `console.svc.plus` returns `403` with `cf-mitigated: challenge` - Cloudflare is blocking the page or auth API before Vercel sees it. -- `console.svc.plus/api/auth/login` returns `404` - Vercel production is not serving the expected Next route, or Cloudflare is pointing at the wrong origin/deployment behavior. -- `accounts.svc.plus/healthz` fails - Back-end outage. Fix backend first. -- `accounts.svc.plus/api/auth/login` returns `200` with `mfaRequired` - Backend is healthy; continue on console/Vercel/Cloudflare. +```bash +docker-compose.yml +Caddyfile +.env.runtime +``` -## Application Checks +Containers: -Verify the current build still contains the auth routes: +- `dashboard`: Next.js standalone runtime on port `3000` +- `frontend-assets`: one-shot task that copies `static/` and `public/` into a shared volume +- `caddy`: TLS termination and reverse proxy + +## GitHub Actions Inputs + +Workflow: + +```text +.github/workflows/service_release_frontend-deploy.yml +``` + +Secrets required: + +- `FRONTEND_DEPLOY_SSH_KEY` +- `OPENCLAW_GATEWAY_TOKEN` if used +- `VAULT_TOKEN` if used +- `AI_GATEWAY_ACCESS_TOKEN` if used +- `INTERNAL_SERVICE_TOKEN` if used +- `CLOUDFLARE_API_TOKEN` if used + +Repository/environment variables recommended: + +- `APP_BASE_URL` +- `NEXT_PUBLIC_APP_BASE_URL` +- `NEXT_PUBLIC_SITE_URL` +- `NEXT_PUBLIC_LOGIN_URL` +- `NEXT_PUBLIC_DOCS_BASE_URL` +- `ACCOUNT_SERVICE_URL` +- `NEXT_PUBLIC_ACCOUNT_SERVICE_URL` +- `SERVER_SERVICE_URL` +- `NEXT_PUBLIC_SERVER_SERVICE_URL` +- `RUNTIME_HOSTNAME` +- `DEPLOYMENT_HOSTNAME` +- `NEXT_PUBLIC_RUNTIME_ENVIRONMENT` +- `NEXT_PUBLIC_RUNTIME_REGION` +- `NEXT_PUBLIC_GISCUS_*` +- `NEXT_PUBLIC_STRIPE_*` +- `NEXT_PUBLIC_PAYPAL_CLIENT_ID` + +## Release Flow + +1. GitHub Actions checks out the repo. +2. GitHub Actions clones `knowledge/`. +3. Docker builds the frontend image with the public `NEXT_PUBLIC_*` values needed at build time. +4. The image is pushed to GHCR. +5. The workflow renders `.env.runtime`. +6. The workflow uploads `docker-compose.yml`, `Caddyfile`, and `.env.runtime` to the host. +7. The host pulls the new image, refreshes the static asset volume, and starts `dashboard + caddy`. +8. The workflow verifies `cn.svc.plus` and `cn.onwalk.net`. + +## Verification Commands + +Local syntax checks: ```bash cd /Users/shenlan/workspaces/cloud-neutral-toolkit/console.svc.plus -yarn build -cat .next/app-path-routes-manifest.json | jq 'with_entries(select(.key|test("/api/auth/")))' +bash -n scripts/github-actions/render-frontend-runtime-env.sh +bash -n scripts/github-actions/deploy-frontend-single-node.sh +cp deploy/single-node/.env.runtime.example deploy/single-node/.env.runtime +docker compose -f deploy/single-node/docker-compose.yml --env-file deploy/single-node/.env.runtime config >/tmp/console-compose.rendered.yaml +rm -f deploy/single-node/.env.runtime +python3 - <<'PY' +from pathlib import Path +import yaml +yaml.safe_load(Path('.github/workflows/service_release_frontend-deploy.yml').read_text()) +print('workflow yaml ok') +PY ``` -Verify the login page still uses same-origin routes: +Remote checks: ```bash -nl -ba 'src/app/(auth)/login/LoginForm.tsx' | sed -n '64,180p' -nl -ba 'src/app/api/auth/login/route.ts' | sed -n '1,180p' -nl -ba 'src/app/api/auth/mfa/verify/route.ts' | sed -n '1,180p' +ssh root@47.120.61.35 "cd /opt/console-svc-plus && docker compose --env-file .env.runtime ps" +ssh root@47.120.61.35 "curl -fsSI -H 'Host: cn.svc.plus' http://127.0.0.1/" +curl -fsSIL https://cn.svc.plus +curl -fsSIL https://cn.onwalk.net ``` -Expected behavior: +## Failure Signatures -- `LoginForm` posts to `/api/auth/login` -- login proxy accepts backend `mfaRequired` / `mfaTicket` -- MFA verify proxy calls `/api/auth/mfa/verify` +- `docker login ghcr.io` fails + The workflow token or package visibility is wrong. +- `frontend-assets` fails + The image layout changed and no longer contains `/app/dashboard/static` or `/app/dashboard/public`. +- `cn.svc.plus` returns `502` + Caddy is up, but the `dashboard` container failed or is not reachable on port `3000`. +- `cn.onwalk.net` does not redirect + Check the deployed `Caddyfile` and domain DNS. -## Vercel Checks +## Rollback -In the Vercel project for `console-svc-plus`, verify: - -1. The production deployment corresponds to the intended git commit. -2. Framework preset is `Next.js`. -3. Build command is `yarn build` or the project default, not a static export command. -4. Output is not being overridden to static export. -5. Production Functions include `app/api/auth/login` and the other `app/api/auth/*` handlers. -6. Required runtime env vars are present for the auth proxy path if they are managed in Vercel. - -If the route exists locally but Vercel returns `404`, suspect: - -- wrong production deployment selected -- wrong root directory/project link -- stale alias or domain assignment -- build output mismatch between local and Vercel - -## Cloudflare Checks - -If `curl` shows `cf-mitigated: challenge`, check Cloudflare first. - -Look for: - -1. Managed Challenge or WAF custom rules affecting `/login` -2. Managed Challenge or WAF custom rules affecting `/api/auth/*` -3. Bot Fight Mode or Super Bot Fight Mode interactions -4. Transform/redirect/cache rules that alter `/api/auth/*` -5. Page Rules or Ruleset Engine policies applied only to the production hostname - -Recommended policy for auth API: - -- Do not cache `/api/auth/*` -- Do not apply JS challenge to `/api/auth/*` -- Keep standard security headers, but let requests reach Vercel - -## Backend Verification - -Use the backend directly to prove whether auth is healthy: +1. Re-run the workflow with a previous known-good image tag. +2. Or update `/opt/console-svc-plus/.env.runtime` and set `FRONTEND_IMAGE=ghcr.io//dashboard:`. +3. Restart the services: ```bash -cd /Users/shenlan/workspaces/cloud-neutral-toolkit/accounts.svc.plus -set -a; source .env; set +a -payload=$(printf '{"identifier":"admin@svc.plus","password":"%s"}' "$SUPERADMIN_PASSWORD") -curl -sS -X POST https://accounts.svc.plus/api/auth/login \ - -H 'Content-Type: application/json' \ - -d "$payload" +ssh root@47.120.61.35 "cd /opt/console-svc-plus && docker compose --env-file .env.runtime run --rm frontend-assets" +ssh root@47.120.61.35 "cd /opt/console-svc-plus && docker compose --env-file .env.runtime up -d dashboard caddy" ``` -Expected for an MFA-enabled admin: - -- HTTP `200` -- response contains `mfaRequired` -- response contains `mfaTicket` or `mfaToken` - -## Known Failure Signatures - -- `POST https://console.svc.plus/api/auth/login 404` - Likely Vercel deployment mismatch or route not published. -- `403` with `cf-mitigated: challenge` - Cloudflare blocked request before Vercel. -- login returns generic failure even though backend returns MFA challenge - Console auth proxy is not parsing MFA fields correctly. -- MFA code accepted by authenticator but web login still fails - Console proxy may be calling the setup endpoint instead of the login MFA endpoint. - -## Rollback Strategy - -When a release breaks auth: - -1. Remove or relax Cloudflare rules affecting `/login` and `/api/auth/*` -2. Re-point domain to last known-good Vercel production deployment -3. Roll back `console.svc.plus` -4. Only then consider `accounts.svc.plus` rollback - -## Related Files - -- `src/app/(auth)/login/LoginForm.tsx` -- `src/app/api/auth/login/route.ts` -- `src/app/api/auth/mfa/status/route.ts` -- `src/app/api/auth/mfa/verify/route.ts` -- `src/server/serviceConfig.ts` +4. Verify `https://cn.svc.plus` again before closing the incident. diff --git a/docs/zh/deployment.md b/docs/zh/deployment.md index 8d13fe3..55f54ea 100644 --- a/docs/zh/deployment.md +++ b/docs/zh/deployment.md @@ -1,31 +1,28 @@ # 部署 -该仓库以 Web 前端体验为主,文档需要覆盖产品流程、界面边界与集成触点。 +## 生产基线 -本页用于统一部署前提、支持的拓扑、运维检查项与回滚注意事项。 +- 运行拓扑: `Caddy + Docker Compose` +- 目标主机: `47.120.61.35` +- 域名: + - `cn.svc.plus` + - `cn.onwalk.net` +- 前端独立发布流水线: `.github/workflows/service_release_frontend-deploy.yml` -## 与当前代码对齐的说明 +## 运行方式 -- 文档目标仓库: `console.svc.plus` -- 仓库类型: `frontend` -- 构建与运行依据: package.json (`dashboard`) -- 主要实现与运维目录: `src/`, `scripts/`, `tests/`, `config/`, `public/` -- `package.json` 脚本快照: `dev`, `prebuild`, `build`, `build:static`, `start`, `lint` +前端镜像在 GitHub Actions 中完成构建并推送到镜像仓库,目标主机只负责拉取镜像和启动容器,不在机器上本地构建。 -## 需要继续归并的现有文档 +当前方案尽量以静态模式运行: + +- Caddy 直接服务 `/_next/static/*` 与 `public/` 里的静态资源。 +- Next.js standalone 容器只承接动态页面、认证接口和代理接口。 +- `knowledge/` 在 CI 阶段拉取,并在 Docker 打包时直接写入镜像。 + +这是针对当前单机弱 IO 环境的权衡。后续如果 `docs.svc.plus` 被拆成独立 API 服务,需要同步调整这里和 `docs/usage/deployment.md` 的镜像内容与路由职责。 + +## 相关文档 -- `development/dev-setup.md` -- `getting-started/installation.md` -- `getting-started/quickstart.md` -- `governance/release-process.md` -- `operations/runbooks/README.md` -- `operations/runbooks/rag-server.md` - `usage/deployment.md` -- `zh/development/dev-setup.md` - -## 本页下一步应补充的内容 - -- 先描述当前已落地实现,再补充未来规划,避免只写愿景不写现状。 -- 术语需要与仓库根 README、构建清单和实际目录保持一致。 -- 将上方列出的历史 runbook、spec、子系统说明逐步链接并归并到本页。 -- 每次发布前,依据当前脚本、清单、CI/CD 流程和环境契约重新核对部署步骤。 +- `governance/release-process.md` +- `development/dev-setup.md` diff --git a/scripts/github-actions/deploy-frontend-single-node.sh b/scripts/github-actions/deploy-frontend-single-node.sh new file mode 100755 index 0000000..7e0a817 --- /dev/null +++ b/scripts/github-actions/deploy-frontend-single-node.sh @@ -0,0 +1,88 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)" +DEPLOY_SOURCE_DIR="${REPO_ROOT}/deploy/single-node" + +require_env() { + local key="$1" + local value="${!key-}" + if [[ -z "${value}" ]]; then + echo "Missing required environment variable: ${key}" >&2 + exit 1 + fi +} + +require_env DEPLOY_HOST +require_env DEPLOY_USER +require_env DEPLOY_DIR +require_env SSH_PRIVATE_KEY +require_env GHCR_USERNAME +require_env GHCR_PASSWORD +require_env FRONTEND_IMAGE +require_env PRIMARY_DOMAIN +require_env SECONDARY_DOMAIN + +WORK_DIR="$(mktemp -d)" +trap 'rm -rf "${WORK_DIR}"' EXIT + +RUNTIME_ENV_FILE="${WORK_DIR}/.env.runtime" +RELEASE_ARCHIVE="${WORK_DIR}/console-svc-plus-release.tgz" +REMOTE_ARCHIVE="/tmp/console-svc-plus-release-${GITHUB_SHA:-manual}.tgz" +SSH_KEY_FILE="${WORK_DIR}/deploy.key" +KNOWN_HOSTS_FILE="${WORK_DIR}/known_hosts" + +bash "${SCRIPT_DIR}/render-frontend-runtime-env.sh" "${RUNTIME_ENV_FILE}" + +cp "${DEPLOY_SOURCE_DIR}/docker-compose.yml" "${WORK_DIR}/docker-compose.yml" +cp "${DEPLOY_SOURCE_DIR}/Caddyfile" "${WORK_DIR}/Caddyfile" + +tar -C "${WORK_DIR}" -czf "${RELEASE_ARCHIVE}" \ + docker-compose.yml \ + Caddyfile \ + .env.runtime + +printf '%s\n' "${SSH_PRIVATE_KEY}" > "${SSH_KEY_FILE}" +chmod 600 "${SSH_KEY_FILE}" +ssh-keyscan -H "${DEPLOY_HOST}" > "${KNOWN_HOSTS_FILE}" + +SSH_BASE=( + ssh + -i "${SSH_KEY_FILE}" + -o StrictHostKeyChecking=yes + -o UserKnownHostsFile="${KNOWN_HOSTS_FILE}" + "${DEPLOY_USER}@${DEPLOY_HOST}" +) + +SCP_BASE=( + scp + -i "${SSH_KEY_FILE}" + -o StrictHostKeyChecking=yes + -o UserKnownHostsFile="${KNOWN_HOSTS_FILE}" +) + +printf '%s' "${GHCR_PASSWORD}" | "${SSH_BASE[@]}" "docker login ghcr.io -u '${GHCR_USERNAME}' --password-stdin" + +"${SCP_BASE[@]}" "${RELEASE_ARCHIVE}" "${DEPLOY_USER}@${DEPLOY_HOST}:${REMOTE_ARCHIVE}" + +"${SSH_BASE[@]}" \ + "DEPLOY_DIR='${DEPLOY_DIR}' REMOTE_ARCHIVE='${REMOTE_ARCHIVE}' PROJECT_NAME='console-svc-plus' bash -s" <<'EOF' +set -euo pipefail + +tmp_dir="$(mktemp -d)" +trap 'rm -rf "${tmp_dir}" "${REMOTE_ARCHIVE}"' EXIT + +mkdir -p "${DEPLOY_DIR}" +tar -xzf "${REMOTE_ARCHIVE}" -C "${tmp_dir}" + +install -m 0644 "${tmp_dir}/docker-compose.yml" "${DEPLOY_DIR}/docker-compose.yml" +install -m 0644 "${tmp_dir}/Caddyfile" "${DEPLOY_DIR}/Caddyfile" +install -m 0600 "${tmp_dir}/.env.runtime" "${DEPLOY_DIR}/.env.runtime" + +cd "${DEPLOY_DIR}" +docker compose --project-name "${PROJECT_NAME}" --env-file .env.runtime pull dashboard caddy +docker compose --project-name "${PROJECT_NAME}" --env-file .env.runtime run --rm frontend-assets +docker compose --project-name "${PROJECT_NAME}" --env-file .env.runtime up -d --remove-orphans dashboard caddy +docker compose --project-name "${PROJECT_NAME}" --env-file .env.runtime ps +EOF diff --git a/scripts/github-actions/render-frontend-runtime-env.sh b/scripts/github-actions/render-frontend-runtime-env.sh new file mode 100755 index 0000000..cddcdaf --- /dev/null +++ b/scripts/github-actions/render-frontend-runtime-env.sh @@ -0,0 +1,76 @@ +#!/usr/bin/env bash +set -euo pipefail + +OUTPUT_PATH="${1:?usage: render-frontend-runtime-env.sh }" + +mkdir -p "$(dirname "${OUTPUT_PATH}")" +: > "${OUTPUT_PATH}" + +append_env() { + local key="$1" + local value="${2-}" + printf '%s=%s\n' "${key}" "${value}" >> "${OUTPUT_PATH}" +} + +require_env() { + local key="$1" + local value="${!key-}" + if [[ -z "${value}" ]]; then + echo "Missing required environment variable: ${key}" >&2 + exit 1 + fi +} + +require_env FRONTEND_IMAGE +require_env PRIMARY_DOMAIN +require_env SECONDARY_DOMAIN + +append_env FRONTEND_IMAGE "${FRONTEND_IMAGE}" +append_env PRIMARY_DOMAIN "${PRIMARY_DOMAIN}" +append_env SECONDARY_DOMAIN "${SECONDARY_DOMAIN}" + +append_env NODE_ENV "production" +append_env PORT "${PORT:-3000}" +append_env RUNTIME_ENV "${RUNTIME_ENV:-prod}" +append_env REGION "${REGION:-cn}" +append_env APP_BASE_URL "${APP_BASE_URL:-https://${PRIMARY_DOMAIN}}" +append_env NEXT_PUBLIC_APP_BASE_URL "${NEXT_PUBLIC_APP_BASE_URL:-https://${PRIMARY_DOMAIN}}" +append_env NEXT_PUBLIC_SITE_URL "${NEXT_PUBLIC_SITE_URL:-https://${PRIMARY_DOMAIN}}" +append_env NEXT_PUBLIC_LOGIN_URL "${NEXT_PUBLIC_LOGIN_URL:-https://${PRIMARY_DOMAIN}/login}" +append_env NEXT_PUBLIC_DOCS_BASE_URL "${NEXT_PUBLIC_DOCS_BASE_URL:-https://${PRIMARY_DOMAIN}/docs}" +append_env SESSION_COOKIE_SECURE "${SESSION_COOKIE_SECURE:-true}" +append_env NEXT_PUBLIC_SESSION_COOKIE_SECURE "${NEXT_PUBLIC_SESSION_COOKIE_SECURE:-true}" +append_env RUNTIME_HOSTNAME "${RUNTIME_HOSTNAME:-${PRIMARY_DOMAIN}}" +append_env NEXT_RUNTIME_HOSTNAME "${NEXT_RUNTIME_HOSTNAME:-${PRIMARY_DOMAIN}}" +append_env DEPLOYMENT_HOSTNAME "${DEPLOYMENT_HOSTNAME:-${PRIMARY_DOMAIN}}" +append_env NEXT_PUBLIC_RUNTIME_ENVIRONMENT "${NEXT_PUBLIC_RUNTIME_ENVIRONMENT:-prod}" +append_env NEXT_PUBLIC_RUNTIME_REGION "${NEXT_PUBLIC_RUNTIME_REGION:-cn}" +append_env ACCOUNT_SERVICE_URL "${ACCOUNT_SERVICE_URL:-https://accounts.svc.plus}" +append_env NEXT_PUBLIC_ACCOUNT_SERVICE_URL "${NEXT_PUBLIC_ACCOUNT_SERVICE_URL:-${ACCOUNT_SERVICE_URL:-https://accounts.svc.plus}}" +append_env SERVER_SERVICE_URL "${SERVER_SERVICE_URL:-https://api.svc.plus}" +append_env NEXT_PUBLIC_SERVER_SERVICE_URL "${NEXT_PUBLIC_SERVER_SERVICE_URL:-${SERVER_SERVICE_URL:-https://api.svc.plus}}" +append_env SERVER_SERVICE_INTERNAL_URL "${SERVER_SERVICE_INTERNAL_URL-}" +append_env OPENCLAW_GATEWAY_REMOTE_URL "${OPENCLAW_GATEWAY_REMOTE_URL-}" +append_env OPENCLAW_GATEWAY_TOKEN "${OPENCLAW_GATEWAY_TOKEN-}" +append_env VAULT_SERVER_URL "${VAULT_SERVER_URL-}" +append_env VAULT_NAMESPACE "${VAULT_NAMESPACE-}" +append_env VAULT_TOKEN "${VAULT_TOKEN-}" +append_env APISIX_AI_GATEWAY_URL "${APISIX_AI_GATEWAY_URL-}" +append_env AI_GATEWAY_ACCESS_TOKEN "${AI_GATEWAY_ACCESS_TOKEN-}" +append_env INTERNAL_SERVICE_TOKEN "${INTERNAL_SERVICE_TOKEN-}" +append_env CLOUDFLARE_API_TOKEN "${CLOUDFLARE_API_TOKEN-}" +append_env CLOUDFLARE_ACCOUNT_ID "${CLOUDFLARE_ACCOUNT_ID-}" +append_env CLOUDFLARE_WEB_ANALYTICS_SITE_TAG "${CLOUDFLARE_WEB_ANALYTICS_SITE_TAG-}" +append_env CLOUDFLARE_ZONE_TAG "${CLOUDFLARE_ZONE_TAG-}" +append_env ROOT_EMAIL_WHITELIST "${ROOT_EMAIL_WHITELIST:-admin@svc.plus}" +append_env NEXT_PUBLIC_PAYPAL_CLIENT_ID "${NEXT_PUBLIC_PAYPAL_CLIENT_ID-}" +append_env NEXT_PUBLIC_GISCUS_REPO "${NEXT_PUBLIC_GISCUS_REPO:-cloud-neutral-toolkit/console.svc.plus}" +append_env NEXT_PUBLIC_GISCUS_REPO_ID "${NEXT_PUBLIC_GISCUS_REPO_ID-}" +append_env NEXT_PUBLIC_GISCUS_CATEGORY "${NEXT_PUBLIC_GISCUS_CATEGORY:-General}" +append_env NEXT_PUBLIC_GISCUS_CATEGORY_ID "${NEXT_PUBLIC_GISCUS_CATEGORY_ID-}" +append_env NEXT_PUBLIC_STRIPE_PRICE_XSTREAM_PAYGO "${NEXT_PUBLIC_STRIPE_PRICE_XSTREAM_PAYGO-}" +append_env NEXT_PUBLIC_STRIPE_PRICE_XSTREAM_SUBSCRIPTION "${NEXT_PUBLIC_STRIPE_PRICE_XSTREAM_SUBSCRIPTION-}" +append_env NEXT_PUBLIC_STRIPE_PRICE_XSCOPEHUB_PAYGO "${NEXT_PUBLIC_STRIPE_PRICE_XSCOPEHUB_PAYGO-}" +append_env NEXT_PUBLIC_STRIPE_PRICE_XSCOPEHUB_SUBSCRIPTION "${NEXT_PUBLIC_STRIPE_PRICE_XSCOPEHUB_SUBSCRIPTION-}" +append_env NEXT_PUBLIC_STRIPE_PRICE_XCLOUDFLOW_PAYGO "${NEXT_PUBLIC_STRIPE_PRICE_XCLOUDFLOW_PAYGO-}" +append_env NEXT_PUBLIC_STRIPE_PRICE_XCLOUDFLOW_SUBSCRIPTION "${NEXT_PUBLIC_STRIPE_PRICE_XCLOUDFLOW_SUBSCRIPTION-}"