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>
This commit is contained in:
220
docker-compose.yml
Normal file
220
docker-compose.yml
Normal file
@@ -0,0 +1,220 @@
|
||||
# ==============================================================================
|
||||
# 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
|
||||
Reference in New Issue
Block a user