Example of vps deploy with infra split from business. Keeps one mysql instance for all.
104 lines
4.7 KiB
YAML
104 lines
4.7 KiB
YAML
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.<business>
|
|
# .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.<slug>, fill it in
|
|
# with the credentials just created and that business's domain.
|
|
# 3. docker compose -f docker-compose.<business>.yaml --env-file .env.business.<slug> 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
|