8.3 KiB
Console Frontend Single-Node Deployment Design
Scope
- Repository:
console.svc.plus - Target host:
root@cn-console.svc.plus - Public domains:
cn-console.svc.pluscn-console.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
cn-console.svc.plusis 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.plusis 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 prebuildpulls documentation andknowledgecontent, 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 runtimefrontend-assets: one-shot container that copies static files from the image into a Docker volumecaddy: 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_URLNEXT_PUBLIC_SITE_URLNEXT_PUBLIC_LOGIN_URLNEXT_PUBLIC_DOCS_BASE_URLNEXT_PUBLIC_RUNTIME_ENVIRONMENTNEXT_PUBLIC_RUNTIME_REGIONNEXT_PUBLIC_GISCUS_*NEXT_PUBLIC_PAYPAL_CLIENT_IDNEXT_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:
prebuilddepends 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.plusbecomes 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-console.svc.plus
Secondary domain:
cn-console.onwalk.net
Current routing decision:
- Caddy accepts both domains
- requests for
cn-console.onwalk.netare redirected permanently tocn-console.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
- check out repository
- clone
knowledge - build and push
ghcr.io/<owner>/dashboard:<tag> - render
.env.runtime - upload compose/caddy/env files to the host
- log in to GHCR on the host
- pull the new image
- run
frontend-assets - start or refresh
dashboardandcaddy - 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:
- set
FRONTEND_IMAGEto a previous known-good tag - rerun
frontend-assets - restart
dashboardandcaddy - verify
cn-console.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:
SINGLE_NODE_VPS_SSH_PRIVATE_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.examplealigned - 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.plusis 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
varsandsecrets- run the workflow against
root@cn-console.svc.plus
- run the workflow against
- validate DNS, TLS, static assets, login flow, and upstream API proxy behavior
Later
- move docs delivery out of the frontend image after
docs.svc.plusis 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.