feat(console): add single-node frontend release flow

This commit is contained in:
Haitao Pan 2026-03-18 22:54:44 +08:00
parent f7041a1410
commit fea1ab6640
15 changed files with 1010 additions and 161 deletions

15
.dockerignore Normal file
View File

@ -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

View File

@ -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=

View File

@ -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}"

1
.gitignore vendored
View File

@ -55,6 +55,7 @@ coverage/
.env
.env.local
.env.*.local
deploy/single-node/.env.runtime
# Build artifacts
build/

View File

@ -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
# ---------------------------

View File

@ -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=

View File

@ -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}
}
}

View File

@ -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:

View File

@ -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`

View File

@ -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/<owner>/dashboard:<tag>`
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.

5
docs/plans/README.md Normal file
View File

@ -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.

View File

@ -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/<owner>/dashboard:<sha>`.
- 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/<owner>/dashboard:<previous-tag>`.
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.

View File

@ -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`

View File

@ -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

View File

@ -0,0 +1,76 @@
#!/usr/bin/env bash
set -euo pipefail
OUTPUT_PATH="${1:?usage: render-frontend-runtime-env.sh <output-path>}"
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-}"