Files
website/docker-compose.yml
Andrew dfdef3d7f4 feat: docker compose dev environment
Replaces the bare-metal `composer run dev` workflow with a fully
containerized 9-service stack orchestrated by docker compose. Single
command brings up the full app — three subdomains (marketing /
account / admin) reachable via Traefik with TLS, MariaDB + Valkey
+ Mailpit + Vite HMR + Horizon + scheduler all wired in.

Components:
- docker-compose.yml: traefik, app (php-fpm), web (nginx), mariadb,
  valkey, mailpit, vite, horizon, scheduler.
- docker/: Dockerfiles, nginx config, entrypoint scripts.
- Makefile: convenience targets (up / down / logs / shell / migrate
  / seed / test / pint / etc).
- .env.docker.example: template for Docker-stack environment vars
  (separate from website/.env so bare-metal devs aren't disrupted).
- website/vite.config.ts: server.host / origin / hmr / cors hooks
  driven by VITE_HOST / VITE_ORIGIN / VITE_HMR_HOST so the same
  config serves both bare-metal and Docker.
- website/bootstrap/app.php: redirectGuestsTo() now uses
  request()->getScheme() so http: dev hosts don't get force-https
  redirects.
- composer.json: drops laravel/sail (replaced by this stack).
- docs/superpowers/specs/2026-04-25-docker-compose-dev-environment-design.md:
  full design spec.

Bare-metal `composer run dev` workflow stays usable for anyone who
prefers it — Docker stack reads .env.docker, doesn't fight
website/.env.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 22:10:53 -04:00

221 lines
7.2 KiB
YAML

# ==============================================================================
# EZSCALE — Docker Compose dev environment
# Spec: docs/superpowers/specs/2026-04-25-docker-compose-dev-environment-design.md
#
# Quick start:
# cp .env.docker.example .env.docker
# make up
#
# Hostnames (auto-resolve to 127.0.0.1):
# https://ezscale.docker.localhost — marketing
# https://account.ezscale.docker.localhost — customer dashboard
# https://admin.ezscale.docker.localhost — admin panel
# https://account.ezscale.docker.localhost/horizon — Horizon dashboard
# https://mail.ezscale.docker.localhost — Mailpit UI
# https://vite.ezscale.docker.localhost — Vite dev server
# http://localhost:8080 — Traefik dashboard (insecure)
# ==============================================================================
name: ezscale
x-app-base: &app-base
build:
context: ./docker/app
args:
UID: ${UID:-1000}
GID: ${GID:-1000}
image: ezscale/app:dev
env_file:
- .env.docker
volumes:
- ./website:/var/www/html
networks:
- ezscale
depends_on:
mariadb:
condition: service_healthy
valkey:
condition: service_healthy
mailpit:
condition: service_started
services:
# ---------------------------------------------------------------------------
# Edge: Traefik (TLS + routing)
# ---------------------------------------------------------------------------
traefik:
image: traefik:v3.6
restart: unless-stopped
command: []
ports:
- "80:80"
- "443:443"
- "8080:8080"
volumes:
- ./docker/traefik/traefik.yml:/etc/traefik/traefik.yml:ro
- ./docker/traefik/dynamic.yml:/etc/traefik/dynamic.yml:ro
- traefik_certs:/etc/traefik/acme
- /var/run/docker.sock:/var/run/docker.sock:ro
networks:
- ezscale
labels:
- "traefik.enable=true"
# ---------------------------------------------------------------------------
# PHP-FPM application container (php artisan serve replacement)
# ---------------------------------------------------------------------------
app:
<<: *app-base
container_name: ezscale-app
restart: unless-stopped
command: ["php-fpm"]
# ---------------------------------------------------------------------------
# nginx — serves public/, proxies PHP to app:9000
# ---------------------------------------------------------------------------
web:
build:
context: ./docker/nginx
image: ezscale/nginx:dev
restart: unless-stopped
volumes:
- ./website:/var/www/html:ro
- ./docker/nginx/conf.d:/etc/nginx/conf.d:ro
networks:
- ezscale
depends_on:
- app
labels:
- "traefik.enable=true"
- "traefik.docker.network=ezscale"
- "traefik.http.services.ezscale-web.loadbalancer.server.port=80"
- "traefik.http.routers.ezscale-web.rule=Host(`ezscale.docker.localhost`) || Host(`account.ezscale.docker.localhost`) || Host(`admin.ezscale.docker.localhost`)"
- "traefik.http.routers.ezscale-web.entrypoints=web"
# ---------------------------------------------------------------------------
# Vite dev server (HMR)
# ---------------------------------------------------------------------------
vite:
build:
context: ./docker/vite
args:
UID: ${UID:-1000}
GID: ${GID:-1000}
image: ezscale/vite:dev
restart: unless-stopped
working_dir: /var/www/html
environment:
VITE_HOST: "0.0.0.0"
VITE_ORIGIN: "http://vite.ezscale.docker.localhost"
VITE_HMR_HOST: "vite.ezscale.docker.localhost"
volumes:
- ./website:/var/www/html
networks:
- ezscale
labels:
- "traefik.enable=true"
- "traefik.docker.network=ezscale"
- "traefik.http.services.ezscale-vite.loadbalancer.server.port=5173"
- "traefik.http.routers.ezscale-vite.rule=Host(`vite.ezscale.docker.localhost`)"
- "traefik.http.routers.ezscale-vite.entrypoints=web"
# ---------------------------------------------------------------------------
# Horizon — queue worker supervisor (Redis-only)
# ---------------------------------------------------------------------------
horizon:
<<: *app-base
container_name: ezscale-horizon
restart: unless-stopped
command: ["php", "artisan", "horizon"]
stop_signal: SIGTERM
stop_grace_period: 60s
# ---------------------------------------------------------------------------
# Scheduler — runs `schedule:work` in a loop (cron replacement)
# ---------------------------------------------------------------------------
scheduler:
<<: *app-base
container_name: ezscale-scheduler
restart: unless-stopped
command: ["php", "artisan", "schedule:work"]
# ---------------------------------------------------------------------------
# MariaDB — primary database
# ---------------------------------------------------------------------------
mariadb:
image: mariadb:12
restart: unless-stopped
command:
- --character-set-server=utf8mb4
- --collation-server=utf8mb4_unicode_ci
- --skip-character-set-client-handshake
environment:
MARIADB_ROOT_PASSWORD: ${DB_ROOT_PASSWORD:-root}
MARIADB_DATABASE: ${DB_DATABASE:-ezscale_billing}
MARIADB_USER: ${DB_USERNAME:-ezscale}
MARIADB_PASSWORD: ${DB_PASSWORD:-ezscale_local}
volumes:
- mariadb_data:/var/lib/mysql
- ./docker/mariadb/init:/docker-entrypoint-initdb.d:ro
networks:
- ezscale
healthcheck:
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
interval: 5s
timeout: 5s
retries: 20
start_period: 30s
# ---------------------------------------------------------------------------
# Valkey — sessions, cache, queues
# ---------------------------------------------------------------------------
valkey:
image: valkey/valkey:9-alpine
restart: unless-stopped
command: ["valkey-server", "--appendonly", "yes"]
volumes:
- valkey_data:/data
networks:
- ezscale
healthcheck:
test: ["CMD", "valkey-cli", "ping"]
interval: 5s
timeout: 3s
retries: 10
# ---------------------------------------------------------------------------
# Mailpit — SMTP catcher + web UI
# ---------------------------------------------------------------------------
mailpit:
image: axllent/mailpit:v1.29
restart: unless-stopped
environment:
MP_SMTP_AUTH_ACCEPT_ANY: "1"
MP_SMTP_AUTH_ALLOW_INSECURE: "1"
MP_MAX_MESSAGES: "5000"
volumes:
- mailpit_data:/data
networks:
- ezscale
labels:
- "traefik.enable=true"
- "traefik.docker.network=ezscale"
- "traefik.http.services.ezscale-mail.loadbalancer.server.port=8025"
- "traefik.http.routers.ezscale-mail.rule=Host(`mail.ezscale.docker.localhost`)"
- "traefik.http.routers.ezscale-mail.entrypoints=web"
# ==============================================================================
# Volumes & networks
# ==============================================================================
volumes:
mariadb_data:
valkey_data:
mailpit_data:
traefik_certs:
networks:
ezscale:
name: ezscale
driver: bridge