Production stack per DT-07 and DT-08: Caddy (TLS) + Spring Boot API + PostgreSQL on a single VPS.
Public demo stack (same VPS, isolated DB): see Public demo below.
This folder holds two deployable stacks side by side. File paths are referenced by the VPS (crontab, webhook, systemd) — do not move or rename them.
| Stack | Files |
|---|---|
| Production | docker-compose.prod.yml, Caddyfile, deploy.sh, .env.example, webhook/ (CI-triggered redeploy), vps.md |
| Public demo | docker-compose.demo.yml, deploy-demo.sh, reset-demo.sh, cron-reset-demo.sh, publish-demo-client.sh, .env.demo.example, demo-nginx.conf, demo-web/ (published SPA build) |
| Shared | caddy-snippets/ (demo writes its snippet here; Caddy from prod compose loads it) |
- Linux VPS with Docker 24+ and Compose v2
- DNS
Arecords forAPI_DOMAINandAPP_DOMAINpointing to the VPS - GitHub repo with API CI pushing to GHCR (
ghcr.io/<owner>/<repo>/api, e.g.ghcr.io/<owner>/intesity-2/api) - Repository secrets:
DEPLOY_WEBHOOK_URL,DEPLOY_WEBHOOK_SECRET(optional)
-
Clone this repo on the VPS (e.g.
/opt/intensity). -
Configure secrets:
cd deploy cp .env.example .env # Edit .env — strong passwords, real domains, GHCR image path, # INTENSITY_RESEND_* and INTENSITY_APP_BASE_URL for password-reset email
-
Make scripts executable:
chmod +x deploy.sh deploy-demo.sh reset-demo.sh cron-reset-demo.sh publish-demo-client.sh webhook/receive.sh
-
Log in to GHCR on the VPS (once):
echo "$GITHUB_PAT" | docker login ghcr.io -u YOUR_GITHUB_USER --password-stdin
-
Start the stack:
./deploy.sh
If upgrading an older install: the Docker network is now fixed as
intensity(required for demo). One-time recreate if Compose complains about the network:docker compose -f docker-compose.prod.yml --env-file .env down ./deploy.sh
-
Verify:
curl -fsS "https://$API_DOMAIN/actuator/health" curl -fsS "https://$APP_DOMAIN/.well-known/assetlinks.json" curl -fsS "https://$APP_DOMAIN/.well-known/apple-app-site-association"
Update placeholder values in
client/deep-link/.well-known/(Android SHA256 fingerprint, Apple Team ID) before store submission.
On push to master, GitHub Actions builds the Docker image, pushes to GHCR, and POSTs to DEPLOY_WEBHOOK_URL:
{ "image": "ghcr.io/<owner>/intesity-2/api", "sha": "<commit-sha>" }After CI succeeds, SSH to the VPS and run:
cd /opt/intensity/deploy
./deploy.shOr pin a specific SHA:
./deploy.sh ghcr.io/<owner>/intesity-2/api abc123def456Install webhook on the VPS, point it at deploy/webhook/hooks.json, and set:
DEPLOY_WEBHOOK_URL→https://your-vps:9000/hooks/intensity-api-deployDEPLOY_WEBHOOK_SECRET→ same value as indeploy/.env
Example systemd unit:
[Unit]
Description=Intensity deploy webhook
After=network.target
[Service]
ExecStart=/usr/bin/webhook -hooks /opt/intensity/deploy/webhook/hooks.json -port 9000 -verbose
Restart=on-failure
[Install]
WantedBy=multi-user.targetRestrict port 9000 to GitHub Actions egress or protect with a reverse proxy + secret header.
Pin the previous image tag in .env:
API_IMAGE=ghcr.io/<owner>/intesity-2/api:PREVIOUS_SHAThen run ./deploy.sh.
One-time VPS migration notes (previous stack → Intensity) live in vps.md.
Internet :443
↓
Caddy (proxy, production compose)
├── api.<domain> → intensity-api:8080
├── app.<domain> → /.well-known/* (deep links) + /auth/reset-password (static reset page)
├── demo-intensity-api.<domain> → intensity-demo-api:8080 (optional)
└── demo-intensity.<domain> → intensity-demo-web:80 (optional)
Postgres prod + Postgres demo (separate compose projects / volumes)
Compose project names: production network intensity (fixed); demo project intensity-demo.
Same GHCR API image as production, profile demo (seed Leo / Maya / Nico). Does not share the production database or JWT secret.
-
DNS
Arecords forDEMO_API_DOMAINandDEMO_APP_DOMAIN→ VPS. -
Configure demo secrets:
cd /opt/intensity/deploy cp .env.demo.example .env.demo # Edit — use a different POSTGRES_PASSWORD and INTENSITY_JWT_SECRET than production # API_IMAGE should match the production image you want to show
-
Production must already be up (
./deploy.sh) so networkintensityand Caddy exist. -
Build and publish the demo SPA (Node 22+ on the VPS or your machine):
chmod +x publish-demo-client.sh ./publish-demo-client.sh
-
Start demo (writes Caddy snippet + reloads proxy):
chmod +x deploy-demo.sh reset-demo.sh cron-reset-demo.sh ./deploy-demo.sh
-
Verify:
curl -fsS "https://$DEMO_API_DOMAIN/actuator/health" curl -fsSI "https://$DEMO_APP_DOMAIN/" curl -fsSI "https://$DEMO_APP_DOMAIN/join" # OpenAPI UI (demo profile) curl -fsSI "https://$DEMO_API_DOMAIN/swagger-ui/index.html"
Interactive SPA with banner + sample-account shortcuts. Sample login: leo@demo.intensity.app / demo1234 (also Maya / Nico).
Suggested visitor tour
- Sign in as Leo → Groups home → open Leo & Maya or Trio de viagem.
- Play on a box (e.g. Fim de semana does not require everyone; Só nós dois / Rolês da galera do).
- Confirm host → session modal (code / QR). Only one open session per group.
- Switch account (Maya/Nico) → confirm on the yellow banner; Leo uses Go to draw (host-only) → waiting → moment.
- Join on Groups with invite code
DEMOTR(seeded on the trip group, long-lived).
Pin a SHA like production:
./deploy-demo.sh ghcr.io/<owner>/intesity-2/api abc123def456./reset-demo.shCron (03:00 UTC):
0 3 * * * /opt/intensity/deploy/cron-reset-demo.sh >> /var/log/intensity-demo-reset.log 2>&1Production webhook does not restart the demo stack. After a new API image is on GHCR:
./deploy-demo.sh # same SHA/tag as prod if desiredAfter client demo UI changes:
./publish-demo-client.sh
docker compose -f docker-compose.demo.yml --env-file .env.demo up -d web- Deploy API to VPS (
./deploy.shor CI webhook) - Build and submit client store release (
client/STORE_RELEASE.md)
Never ship a client build that calls new API endpoints before the API is live.