Files
website/docker
Andrew 22d1ce3102 feat(docker): production multi-stage Dockerfile
Three named targets (app, horizon, scheduler) sharing a runtime-base
with PHP 8.3-FPM, opcache, redis, and pinned php-fpm pool config.
Composer + Node build stages are separate so vendor/ and public/build/
are baked into the runtime image.

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

EZSCALE — Docker Compose Dev Environment

Multi-service development stack for the EZSCALE Laravel app. Replaces the bare-metal composer run dev workflow with a fully-containerized environment that mirrors the production topology (nginx + PHP-FPM + MariaDB + Valkey + Horizon).

Prerequisites

  • Docker Engine 24+ with Compose V2 (docker compose, not docker-compose)
  • WSL2 with files cloned under /home/$USER/ (NOT /mnt/c/... — bind-mount performance is unusable on the Windows-mounted filesystem)
  • A modern browser (Chrome, Firefox, Safari) — *.docker.localhost resolution requires no /etc/hosts edits

First-time setup

make init

That single command:

  1. Copies .env.docker.example.env.docker
  2. Builds all custom images (PHP-FPM, nginx, vite)
  3. Pulls third-party images (Traefik, MariaDB, Valkey, Mailpit)
  4. Boots MariaDB and Valkey, waits for them to be healthy
  5. Runs composer install and npm install
  6. Brings up the rest of the stack

When make init finishes, you should be able to open:

The first time you visit any of these, your browser will warn about a self-signed cert. Accept once — Traefik issues a wildcard cert covering all subdomains.

Daily commands

make up                       # bring stack up
make down                     # stop (volumes preserved)
make logs                     # tail all logs
make logs SVC=horizon         # tail one service
make sh                       # bash inside app container
make artisan ARGS="migrate"   # any artisan command
make composer ARGS="require foo/bar"
make npm ARGS="install foo"
make test                     # php artisan test --compact
make pint                     # format dirty PHP
make fresh                    # migrate:fresh --seed
make destroy                  # nuclear: stop + wipe volumes

make help prints the full list.

Architecture

Service Image Role
traefik traefik:v3.6 Edge proxy + TLS termination + routing
app custom (PHP 8.3-FPM Debian) Application container
web nginx:1.30-alpine Serves public/, proxies PHP
vite custom (Node 24 Alpine) HMR dev server
horizon same as app Queue worker supervisor
scheduler same as app schedule:work runner
mariadb mariadb:12 Primary database
valkey valkey/valkey:9-alpine Sessions/cache/queues
mailpit axllent/mailpit:v1.29 SMTP catcher

Traefik routes the three Laravel subdomains (marketing/account/admin) to the same nginx container. Laravel's Route::domain() in bootstrap/app.php handles per-subdomain dispatch internally.

Environment

The stack reads .env.docker at the repo root — separate from website/.env. This keeps the Docker workflow from fighting any bare-metal composer run dev setup you might still want to use.

Critical Docker-specific values:

DB_HOST=mariadb
REDIS_HOST=valkey
MAIL_HOST=mailpit
APP_URL=https://ezscale.docker.localhost

Third-party API keys (Stripe, PayPal, VirtFusion, etc.) need to be added to your .env.docker if you want to test those integrations.

Volumes

Persisted across make down (lost only on make destroy):

  • mariadb_data — MySQL data
  • valkey_data — Valkey AOF persistence
  • mailpit_data — captured email
  • traefik_certs — self-signed cert cache

The Laravel source (./website) is bind-mounted live — your edits show up immediately. vendor/ and node_modules/ are visible on the host for IDE autocomplete.

TLS

Traefik auto-generates a single self-signed wildcard cert for *.ezscale.docker.localhost on first boot. The cert lives in the traefik_certs volume.

If you want a green-padlock experience instead of accepting the warning once per browser:

brew install mkcert  # or apt/winget equivalent
mkcert -install
mkcert -cert-file docker/traefik/certs/cert.pem \
       -key-file  docker/traefik/certs/key.pem \
       "ezscale.docker.localhost" "*.ezscale.docker.localhost"

Then update docker/traefik/dynamic.yml to point certificates: at those files. Currently configured for self-signed; mkcert is left as a future enhancement.

Common gotchas

Port 80 or 443 already in use. Edit docker-compose.yml's traefik service and remap to e.g. 8000:80, 8443:443. Then access the stack at https://ezscale.docker.localhost:8443.

make init hangs on "waiting for mariadb". First MariaDB boot creates the system tablespace and can take 20-30s. The healthcheck has a 30s start_period to accommodate this. If it really stalls, make logs SVC=mariadb to see why.

Permission errors on storage/logs/laravel.log. The PHP container's UID matches your host UID (1000) by default. If your host UID differs, rebuild with UID=$(id -u) GID=$(id -g) make build.

Horizon dashboard 403s. Horizon's gate is in App\Providers\HorizonServiceProvider::gate(). In dev all admin users have access; you need to log in with an admin role first.

Vite assets don't load. Check make logs SVC=vite. The Laravel Vite plugin auto-injects the dev URL — if it can't reach https://vite.ezscale.docker.localhost, assets fall back to the manifest. Make sure that hostname is reachable in your browser.

Composer/npm install slow. First-time composer install takes 1-2 min. After that the vendor/ dir is cached on disk. Same for node_modules.

Co-existing with bare-metal dev

This stack does NOT delete or modify website/.env. If you previously used cd website && composer run dev, that still works — it reads website/.env and connects to whatever local PHP/MySQL/Redis you had.

Pick one or the other for any given session. Don't run both simultaneously (they'd fight over ports and sessions).

Spec

Full design rationale in docs/superpowers/specs/2026-04-25-docker-compose-dev-environment-design.md.