# ============================================================================== # 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