name: next-chair-infra # Shared infrastructure for the multi-business VPS deployment: Caddy # (reverse proxy + automatic TLS via Let's Encrypt), one MySQL server # shared by every business (a separate database + user per business, NOT # one shared schema — see step 1 below), and Dockge (a web UI for # starting/stopping/inspecting these compose stacks by hand). # # Lives in stacks/infra/ so Dockge (which expects one directory per # stack, each with its own compose.yaml) can manage this stack too — # see stacks/blake-heritage/, stacks/marios/ for the same shape per # business. Deliberately separate from docker-compose.local.yaml at the # repo root, which is the single-business local/Docker-Desktop setup and # stays untouched by any of this. # # ── One-time setup ─────────────────────────────────────────────────────── # docker network create next-chair-edge # cp .env.infra.example stacks/infra/.env (then fill in real values — # in particular STACKS_DIR must be this repo's absolute path on the # VPS, e.g. /opt/nextchair/stacks — see the STACKS_DIR comment in that # file for why it can't just be a relative path here) # docker compose -f stacks/infra/compose.yaml --env-file stacks/infra/.env up -d # # ── Per business ────────────────────────────────────────────────────── # Use the management UI (manager/) once it's running — it automates all # of the steps below. To do it by hand instead: # 1. Create that business's database + user on the shared MySQL server. # There's no automatic way to do this per business via env vars — # the mysql:8.0 image's MYSQL_DATABASE/USER/PASSWORD vars only # provision ONE database at first boot, and /docker-entrypoint- # initdb.d scripts only ever run once against an empty data # directory — so this one step is manual, run once per business: # docker exec -it next-chair-mysql mysql -uroot -p # CREATE DATABASE blake_heritage CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci; # CREATE USER 'blake_heritage'@'%' IDENTIFIED BY 'choose-a-strong-password'; # GRANT ALL PRIVILEGES ON blake_heritage.* TO 'blake_heritage'@'%'; # FLUSH PRIVILEGES; # 2. Copy .env.business.example to stacks//.env, fill it in with # the credentials just created and that business's domain. # 3. Add stacks/infra/sites/.caddy (see that directory's own # note) and reload Caddy: # docker exec next-chair-caddy caddy reload --config /etc/caddy/Caddyfile # 4. docker compose -f stacks//compose.yaml --env-file stacks//.env up -d services: caddy: image: caddy:2-alpine container_name: next-chair-caddy restart: unless-stopped env_file: - .env ports: # Overridable for local testing (a machine already running # something else on 80/443) — a real deployment leaves these at # their defaults, since Let's Encrypt's HTTP challenge and normal # HTTPS traffic both expect the standard ports. - "${CADDY_HTTP_PORT:-80}:80" - "${CADDY_HTTPS_PORT:-443}:443" volumes: - ./Caddyfile:/etc/caddy/Caddyfile:ro # One .caddy file per business, imported by the Caddyfile — see # sites/.gitkeep's own note. Adding/removing a file here needs a # `caddy reload` (see the per-business setup steps above) to take # effect; Caddy doesn't watch this directory on its own. - ./sites:/etc/caddy/sites # Caddy's own automatic-HTTPS certificate storage — must persist # across restarts or every restart re-issues every certificate # against Let's Encrypt's rate limits. - next-chair-caddy-data:/data - next-chair-caddy-config:/config networks: - next-chair-edge next-chair-mysql: image: mysql:8.0 container_name: next-chair-mysql restart: unless-stopped env_file: - .env # No MYSQL_DATABASE/MYSQL_USER/MYSQL_PASSWORD here deliberately — this # server hosts one database per business (see step 1 above), not one # shared schema. Only MYSQL_ROOT_PASSWORD (in .env) is required for # the image to boot. command: --character-set-server=utf8mb4 --collation-server=utf8mb4_0900_ai_ci ports: # Host-machine access (e.g. a GUI client over an SSH tunnel) — every # business's app container reaches this over the shared Docker # network via the service name `next-chair-mysql`, never this. - "${MYSQL_HOST_PORT:-3306}:3306" volumes: - next-chair-mysql-data:/var/lib/mysql healthcheck: test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-u", "root", "-p${MYSQL_ROOT_PASSWORD:-change-me-too}"] interval: 5s timeout: 5s retries: 20 start_period: 10s networks: - next-chair-edge dockge: image: louislam/dockge:1 container_name: next-chair-dockge restart: unless-stopped env_file: - .env environment: DOCKGE_STACKS_DIR: "${STACKS_DIR}" ports: - "${DOCKGE_HOST_PORT:-5001}:5001" volumes: - /var/run/docker.sock:/var/run/docker.sock - next-chair-dockge-data:/app/data # MUST be mounted at the identical absolute path on both host and # container — see the STACKS_DIR comment in .env.infra.example for # why (Dockge shells `docker compose` out to the HOST's dockerd via # the socket above, so a compose-file path only means the same # thing on both sides if the bind-mount doesn't change it). - "${STACKS_DIR}:${STACKS_DIR}" networks: - next-chair-edge volumes: next-chair-mysql-data: name: next-chair-mysql-data next-chair-caddy-data: name: next-chair-caddy-data next-chair-caddy-config: name: next-chair-caddy-config next-chair-dockge-data: name: next-chair-dockge-data networks: next-chair-edge: name: next-chair-edge external: true