2026-03-17 16:22:42 +08:00
# Deployment Runbook
2026-01-26 21:39:00 +08:00
2026-03-17 16:22:42 +08:00
## Scope
2026-01-26 21:39:00 +08:00
2026-03-17 16:22:42 +08:00
- Runtime: `console.svc.plus`
2026-03-18 22:54:44 +08:00
- 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`
2026-01-26 21:39:00 +08:00
2026-03-18 22:54:44 +08:00
## Current Delivery Model
2026-01-26 21:39:00 +08:00
2026-03-18 22:54:44 +08:00
The production frontend is deployed as a prebuilt container image from GitHub Actions.
2026-03-17 16:22:42 +08:00
2026-03-18 22:54:44 +08:00
- 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` .
2026-03-20 00:11:44 +08:00
- `/docs` and `/blogs` fetch their content from `docs.svc.plus` at runtime; the frontend image no longer packs `knowledge/` or synced markdown payloads.
2026-03-18 22:54:44 +08:00
- Static assets are extracted from the image into a shared Docker volume so Caddy can serve `/_next/static/*` and checked-in public files directly.
2026-03-17 16:22:42 +08:00
2026-03-20 00:11:44 +08:00
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, but docs/blog content delivery is now delegated to `docs.svc.plus` .
2026-03-17 16:22:42 +08:00
2026-03-18 23:16:57 +08:00
## Control Plane & DNS Stage
2026-03-19 23:57:54 +08:00
The control repo (`github-org-x-evor`) tracks `console.svc.plus` through `console.svc.plus.code-workspace` and keeps the `subrepos/accounts.svc.plus` pointer in sync via `skills/cross-repo-upstream-submodule-sync` . Releases resolve metadata with that workspace and the `config/single-node-release` manifests. After `.github/workflows/service_release_frontend-deploy.yml` finishes pushing the new image, the control-plane workflow `.github/workflows/service_release_apiserver-deploy.yml` calls `scripts/github-actions/update-release-dns.sh` to update Cloudflare DNS so the new endpoint is reachable under `cn.svc.plus` and `cn.onwalk.net` .
2026-03-18 23:16:57 +08:00
2026-03-18 22:54:44 +08:00
## Runtime Layout
2026-03-17 16:22:42 +08:00
2026-03-18 22:54:44 +08:00
Remote directory:
2026-03-17 16:22:42 +08:00
```bash
2026-03-18 22:54:44 +08:00
/opt/console-svc-plus
2026-03-17 16:22:42 +08:00
```
2026-03-18 22:54:44 +08:00
Files deployed there:
2026-03-17 16:22:42 +08:00
```bash
2026-03-18 22:54:44 +08:00
docker-compose.yml
Caddyfile
.env.runtime
2026-03-17 16:22:42 +08:00
```
2026-03-18 22:54:44 +08:00
Containers:
2026-03-17 16:22:42 +08:00
2026-03-18 22:54:44 +08:00
- `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
2026-03-17 16:22:42 +08:00
2026-03-18 22:54:44 +08:00
## GitHub Actions Inputs
2026-03-17 16:22:42 +08:00
2026-03-18 22:54:44 +08:00
Workflow:
2026-03-17 16:22:42 +08:00
2026-03-18 22:54:44 +08:00
```text
.github/workflows/service_release_frontend-deploy.yml
```
2026-03-17 16:22:42 +08:00
2026-03-18 22:54:44 +08:00
Secrets required:
2026-03-19 08:37:42 +08:00
- `SINGLE_NODE_VPS_SSH_PRIVATE_KEY`
2026-03-18 22:54:44 +08:00
- `OPENCLAW_GATEWAY_TOKEN` if used
- `VAULT_TOKEN` if used
- `AI_GATEWAY_ACCESS_TOKEN` if used
- `INTERNAL_SERVICE_TOKEN` if used
2026-03-19 07:50:25 +08:00
- `CLOUDFLARE_DNS_API_TOKEN` for the Cloudflare DNS stage
- `CLOUDFLARE_API_TOKEN` if homepage Cloudflare analytics are enabled at runtime
2026-03-18 22:54:44 +08:00
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`
2026-03-20 00:11:44 +08:00
- `DOCS_SERVICE_URL`
- `DOCS_SERVICE_INTERNAL_URL`
2026-03-18 22:54:44 +08:00
- `NEXT_PUBLIC_RUNTIME_ENVIRONMENT`
- `NEXT_PUBLIC_RUNTIME_REGION`
- `NEXT_PUBLIC_GISCUS_*`
- `NEXT_PUBLIC_STRIPE_*`
- `NEXT_PUBLIC_PAYPAL_CLIENT_ID`
2026-03-19 08:04:00 +08:00
- `CLOUDFLARE_ZONE_TAG` if homepage Cloudflare analytics are enabled at runtime
- `CLOUDFLARE_DNS_ZONE_TAG` only for single-domain manual DNS override; the GitHub Actions DNS stage resolves zones from each domain automatically
2026-03-18 22:54:44 +08:00
## Release Flow
1. GitHub Actions checks out the repo.
2026-03-20 00:11:44 +08:00
2. Docker builds the frontend image with the public `NEXT_PUBLIC_*` values needed at build time.
3. The image is pushed to GHCR.
4. The workflow runs a matrix DNS stage, updating one public domain per job.
5. The workflow renders `.env.runtime` , including docs service runtime endpoints.
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` .
2026-03-18 22:54:44 +08:00
## Verification Commands
Local syntax checks:
2026-03-17 16:22:42 +08:00
```bash
2026-03-18 22:54:44 +08:00
cd /Users/shenlan/workspaces/cloud-neutral-toolkit/console.svc.plus
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
2026-03-17 16:22:42 +08:00
```
2026-03-18 22:54:44 +08:00
Remote checks:
2026-03-17 16:22:42 +08:00
2026-03-18 22:54:44 +08:00
```bash
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
```
2026-03-17 16:22:42 +08:00
2026-03-18 22:54:44 +08:00
## Failure Signatures
2026-03-17 16:22:42 +08:00
2026-03-18 22:54:44 +08:00
- `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.
2026-03-17 16:22:42 +08:00
2026-03-18 22:54:44 +08:00
## Rollback
2026-03-17 16:22:42 +08:00
2026-03-18 22:54:44 +08:00
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:
2026-03-17 16:22:42 +08:00
2026-03-18 22:54:44 +08:00
```bash
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"
```
2026-03-17 16:22:42 +08:00
2026-03-18 22:54:44 +08:00
4. Verify `https://cn.svc.plus` again before closing the incident.