name: next-chair-infra # Shared infrastructure for a multi-business VPS deployment: one Traefik # reverse proxy (routes each business's domain to its own app container, # handles TLS via Let's Encrypt) and one MySQL server shared by every # business — a separate database + user per business, NOT one shared # schema (see step 1 below). # # Deliberately separate from docker-compose.local.yaml, which is the # single-business local/Docker-Desktop setup and stays untouched by this. # # ── One-time setup ─────────────────────────────────────────────────────── # docker network create next-chair-edge # cp .env.infra.example .env.infra (then fill in real values) # docker compose -f docker-compose.infra.yaml --env-file .env.infra up -d # # ── Per business, after infra is running (see docker-compose. # .yaml — this repo ships one per business, e.g. docker-compose.blake- # heritage.yaml) ──────────────────────────────────────────────────────── # 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 .env.business., fill it in # with the credentials just created and that business's domain. # 3. docker compose -f docker-compose..yaml --env-file .env.business. up -d --build services: traefik: image: traefik:v3.2 container_name: next-chair-traefik restart: unless-stopped command: - "--providers.docker=true" # Only containers with an explicit traefik.enable=true label are # ever routed to — nothing becomes reachable by accident just for # sitting on the shared network. - "--providers.docker.exposedbydefault=false" - "--entrypoints.web.address=:80" - "--entrypoints.websecure.address=:443" # Plain HTTP always redirects to HTTPS — no business is ever served # unencrypted, even before you've thought about it per-business. - "--entrypoints.web.http.redirections.entrypoint.to=websecure" - "--entrypoints.web.http.redirections.entrypoint.scheme=https" - "--certificatesresolvers.letsencrypt.acme.httpchallenge=true" - "--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web" - "--certificatesresolvers.letsencrypt.acme.email=${ACME_EMAIL}" - "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json" ports: - "80:80" - "443:443" volumes: # Read-only — Traefik only needs to observe container labels/state # to discover routes, never control the daemon. - /var/run/docker.sock:/var/run/docker.sock:ro - next-chair-traefik-certs:/letsencrypt networks: - next-chair-edge next-chair-mysql: image: mysql:8.0 container_name: next-chair-mysql restart: unless-stopped env_file: - .env.infra # 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.infra) 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 volumes: next-chair-mysql-data: name: next-chair-mysql-data next-chair-traefik-certs: name: next-chair-traefik-certs networks: next-chair-edge: name: next-chair-edge external: true