Deployment Redesign
CI / ci (push) Canceled after 0s

Implemented the deployment redesign.
- Added Dockge-first-class stacks:
  - [stacks/infra/compose.yaml]
  - [stacks/caddy/compose.yaml]
- Added reusable business template: [deploy/business-compose.yaml.template]
- Added automated provisioning: [scripts/create-business.sh]
- Added per-business database/user, secrets, media volume, private networks, and Caddy route generation.
- Removed Traefik and manual business Compose files.
- Updated environment templates, .gitignore, and deployment documentation in [README.md]
Validation completed:
- Docker Compose config passed for infra, Caddy, and generated business stacks.
- Shell syntax validation passed.
- Registry image tags containing / were tested.
- No remaining Traefik or obsolete deployment references found.
This commit is contained in:
Stefan Lazic committed 2026-09-04 20:32:06 +01:00
1 parent 5472db320e
commit 764d087eac
13 files changed
+289 -380

No files matched your search

+16 -99
View File
@@ -1,100 +1,17 @@
# One business's config on the shared multi-business VPS setup — see
# docker-compose.infra.yaml for the one-time setup this depends on
# (creating this business's database on the shared MySQL server) and
# docker-compose.<business>.yaml for how this file is used. Copy this to
# `.env.business.<slug>` (e.g. `.env.business.blake-heritage` — kept out
# of git, see .gitignore) per business and fill in real values. Nothing
# here is a real credential.
#
# The app starts and runs a complete first-install flow with ONLY the
# "Required for basic startup" section below filled in — every other
# integration (email, push, Google sign-in, Google Calendar) shows a
# disabled state rather than crashing when its variables are left blank.
# ── Required for basic startup ─────────────────────────────────────────
# The public domain this business is served on — used by Traefik's
# routing label (see docker-compose.<business>.yaml) to decide which
# container handles requests for it, so this must be set and must match
# the domain's real DNS (pointed at the VPS) before Traefik can issue a
# certificate for it.
DOMAIN="blake-heritage.example.com"
# The app reaches MySQL via the shared infra's service hostname
# `next-chair-mysql`, never `localhost` — see docker-compose.infra.yaml.
# DATABASE_NAME/USER/PASSWORD must match exactly what you created for
# this business on the shared server (see docker-compose.infra.yaml's
# "CREATE DATABASE ..." step) — this file doesn't create them, it only
# tells the app what to connect as.
DATABASE_HOST="next-chair-mysql"
DATABASE_PORT="3306"
DATABASE_NAME="blake_heritage"
DATABASE_USER="blake_heritage"
# Generated business environment file. Run
# `./scripts/create-business.sh <slug> <domain>` instead of editing a copy.
DOMAIN="example.com"
DATABASE_HOST="nextchair-mysql"
DATABASE_PORT=3306
DATABASE_NAME="nextchair_example_business"
DATABASE_USER="nextchair_example_business"
DATABASE_PASSWORD="replace-me"
# Generate with: npx auth secret
AUTH_SECRET="replace-me-with-a-32-byte-random-string"
# Auth.js needs to trust the host it's served from when not on localhost:3000.
AUTH_TRUST_HOST="true"
NEXT_PUBLIC_APP_URL="https://blake-heritage.example.com"
# One-time setup token the installation wizard (/install) requires before
# it will create the first business/administrator — generate with:
# openssl rand -hex 32
INSTALLATION_TOKEN="replace-me"
# Shared secret an external scheduler presents to
# /api/cron/process-notifications — generate with: openssl rand -hex 32
CRON_SECRET="replace-me"
# ── Super Administrator (optional — see scripts/ensure-super-admin.ts) ──
# Platform-level account with every Owner permission. Not run
# automatically by this compose file (that's a Docker-entrypoint-only
# behaviour, see docker/entrypoint.sh) — run it manually, once, the same
# way migrations were run for the Vercel/Hostinger deployment earlier:
# docker exec -it next-chair-blake-heritage npx tsx scripts/ensure-super-admin.ts
# Leave SUPER_ADMIN_EMAIL blank to skip entirely and use the /install
# wizard's own administrator account instead.
SUPER_ADMIN_EMAIL=""
SUPER_ADMIN_PASSWORD=""
SUPER_ADMIN_FIRST_NAME=""
SUPER_ADMIN_LAST_NAME=""
# ── Google sign-in (optional — "Continue with Google" on Sign In) ─────
AUTH_GOOGLE_ID=""
AUTH_GOOGLE_SECRET=""
# ── Google Calendar sync (optional — staff-level, see /staff/settings/calendar) ─
# From https://console.cloud.google.com/apis/credentials — a separate
# OAuth client from AUTH_GOOGLE_ID/SECRET above (different scopes).
GOOGLE_CALENDAR_CLIENT_ID=""
GOOGLE_CALENDAR_CLIENT_SECRET=""
GOOGLE_CALENDAR_REDIRECT_URI="https://blake-heritage.example.com/api/staff/calendar/google/callback"
# Encrypts stored refresh tokens at rest — generate with: openssl rand -hex 32
CALENDAR_TOKEN_ENCRYPTION_KEY=""
# ── Media Manager (optional — see src/lib/media/media-storage.ts) ──────
# Max upload size in bytes for the Media Manager. Defaults to 5,000,000
# (5MB) if unset.
MEDIA_MAX_UPLOAD_BYTES="5000000"
# ── Google Reviews (optional — see /owner/settings/reviews) ────────────
# From https://console.cloud.google.com/apis/credentials — needs "Places
# API (New)" enabled on that project (a different product from the older
# "Places API" — enabling the wrong one returns REQUEST_DENIED). Leave
# blank to keep review sync disabled entirely (Home shows clearly-labelled
# demo reviews instead).
GOOGLE_PLACES_API_KEY=""
# ── Email (optional — defaults to logging to the console instead of sending) ─
EMAIL_PROVIDER="console"
EMAIL_SMTP_HOST="smtp-relay.brevo.com"
EMAIL_SMTP_PORT="587"
EMAIL_SMTP_USERNAME=""
EMAIL_SMTP_PASSWORD=""
EMAIL_FROM_ADDRESS=""
EMAIL_FROM_NAME=""
# ── Web Push (optional — defaults to logging to the console instead of sending) ─
# Generate with: npx web-push generate-vapid-keys
PUSH_PROVIDER="console"
VAPID_PUBLIC_KEY=""
NEXT_PUBLIC_VAPID_PUBLIC_KEY=""
VAPID_PRIVATE_KEY=""
VAPID_SUBJECT="mailto:owner@example.com"
DATABASE_CONNECTION_LIMIT=5
DATABASE_CONNECT_TIMEOUT_MS=10000
DATABASE_SSL=false
AUTH_SECRET="replace-with-a-long-random-secret"
AUTH_TRUST_HOST=true
NEXT_PUBLIC_APP_URL="https://example.com"
INSTALLATION_TOKEN="replace-with-a-long-random-token"
CRON_SECRET="replace-with-a-long-random-secret"
NEXTCHAIR_IMAGE="nextchair:latest"