Skip to content

Deployment and static SPAs

ScriptWhat it runs
npm run admin:buildturbo run build --filter=@indochina/admin-web
npm run vendor:buildturbo run build --filter=@indochina/vendor-web
npm run admin:build:stagingVite --mode staging + VITE_API_URL (see apps/admin-web/staging.env.example)
npm run vendor:build:stagingVite --mode staging + VITE_API_URL (see apps/vendor-web/staging.env.example)
npm run deploy:stagingdeploy-to-vps-staging.sh — staging SSH env, backend .env.host.staging, staging SPA builds
npm run backend:buildturbo run build --filter=@indochina/backend
npm run build:deployadmin:build → vendor:build → backend:build (single command for release)
npm run deploybash deploy/deploy-to-vps.sh (optional VPS rsync; see below)
npm run docs:checkapps/docs/scripts/check-docs.mjs — runs astro check + the docs ↔ e2e screenshot contract linter
npm run docs:check:screenshotsapps/docs/scripts/check-docs-screenshots.mjs only (skip astro)
npm run docs:make:white-logoRegenerate apps/vendor-web/public/logo-white.png from logo.png (transparent-bg, white marks)
npm run docs:deployapps/docs/scripts/deploy-docs.sh — pre-flight checks, astro build, rsync to the docs host (uses deploy/.env.deploy)
npm run docs:deploy:stagingSame as above with deploy/.env.deploy.staging

Vite outDir in each app’s vite.config.ts points to apps/backend/static/admin and apps/backend/static/vendor, so SPA builds write straight into the Nest static tree before backend:build.

The root Dockerfile:

  1. Builder: npm ci, copy workspaces, set NODE_ENV=production and VITE_API_URL=, run npm run build:deploy.
  2. Runner: production npm install --omit=dev for apps/backend only, copy dist and static, run node dist/main.js.

So CI “copy dist into static” is already encoded in the image build — no manual rsync inside the container.

  • Runtime: Nest writes gallery files under PUBLIC_STORAGE_ROOT (default {cwd}/public/storage) and serves them at /public/storage/* (mountPublicStorage in bootstrap-static-spas.ts). Clover import jobs use VENDOR_IMPORT_STORAGE_ROOT (default {cwd}/storage/vendor-imports).
  • Docker: deploy/docker-compose.yml mounts named volumes on those default paths so uploads survive image rebuilds.
  • PM2/VPS: set PUBLIC_STORAGE_ROOT and VENDOR_IMPORT_STORAGE_ROOT in apps/backend/.env.host to directories outside the rsync deploy tree; see deploy/README.md → Persistent uploads.
  • .github/workflows/docker-build.yml — validates the image on main/master and PRs (build only, no registry push).
  • deploy/deploy-to-vps.sh — builds locally (unless SKIP_BUILD=1), rsyncs static/* and/or dist to the server, optional RUN_MIGRATIONS=1, PM2 or systemd restart. Configure via deploy/.env.deploy.

The docs are an Astro/Starlight static site — they are not served by the Nest API and are not part of npm run build:deploy. They live in apps/docs/dist/ after a build and are deployed independently via npm run docs:deploy.

CommandWhat it does
npm run docs:devAstro dev server (default localhost:4321)
npm run docs:buildProduction build → apps/docs/dist/
npm run docs:checkRun astro check (TS / content collection validation) plus the docs ↔ e2e screenshot contract linter in one shot

The deploy script lives at apps/docs/scripts/deploy-docs.sh and reuses the same SSH / rsync conventions as deploy/deploy-to-vps.sh. Configure once:

  1. Copy the env example and fill in docs-only fields:

    Terminal window
    cp deploy/env.deploy.example deploy/.env.deploy
    # edit deploy/.env.deploy — see the "Docs site" block at the bottom:
    # DOCS_DEPLOY_REMOTE_PATH=/var/www/docs.indochinaenterprises.com
    # DOCS_DEPLOY_HOST=docs.indochinaenterprises.com (optional override)
    # DOCS_DEPLOY_USER=deploy (optional override)
    # DOCS_PUBLIC_URL=https://docs.indochinaenterprises.com
  2. Ensure the docs server is reachable over SSH (ssh-copy-id deploy@docs.indochinaenterprises.com).

  3. Run from the repo root:

    Terminal window
    npm run docs:deploy # production
    npm run docs:deploy:staging # staging (uses deploy/.env.deploy.staging)

What the script does:

  1. Pre-flight: astro check + the docs ↔ e2e screenshot contract linter. Aborts the deploy on any failure.
  2. Build: astro build (honors DOCS_PUBLIC_URL for canonical URLs + sitemap-index.xml). Set DOCS_DEPLOY_SKIP_BUILD=1 to reuse an existing apps/docs/dist/.
  3. Rsync: apps/docs/dist/dot → DOCS_DEPLOY_REMOTE_PATH/ on the docs host with --delete so the deploy is idempotent.

The target directory is whatever you put behind Nginx / Caddy / a CDN — it does not need to be public/docs. Typical setups:

  • Bare Nginx server root: DOCS_DEPLOY_REMOTE_PATH=/var/www/docs.indochinaenterprises.com.
  • Sub-path on the API host: DOCS_DEPLOY_REMOTE_PATH=${DEPLOY_REMOTE_PATH}/public/docs (the default), then point your reverse proxy at /docs/*.
  • apps/vendor-web/public/logo.png and logo-white.png are version-controlled PNGs committed to the repo. The white variant is regenerated by npm run docs:make:white-logo (uses the colored file as input). Both files are served from /logo.png and /logo-white.png via Vite’s public/ mount — no code changes are needed after a swap.
  • Favicons reference /logo.png in apps/vendor-web/index.html, apps/vendor-web/index-cashier.html, and apps/vendor-web/index-customer-display.html. Update the PNG, then rebuild the vendor web (npm run vendor:build) so the new asset ships in the SPA bundle.
  • When index.html exists under apps/backend/static/admin and static/vendor, the backend serves them (see bootstrap-static-spas.ts).
  • Reserved paths for the vendor SPA exclude /api, /admin, and Swagger UI/JSON paths so APIs keep working.

Document Nginx or CDN rules (TLS, caching, WebSocket if added) in your internal runbook.