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:
2026-04-26 22:10:53 -04:00
parent 4b98e52043
commit dfdef3d7f4
21 changed files with 1975 additions and 741 deletions

142
docker/README.md Normal file
View File

@@ -0,0 +1,142 @@
# 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
```bash
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:
- **https://ezscale.docker.localhost** — marketing site
- **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 (catches all outgoing email)
- **https://vite.ezscale.docker.localhost** — Vite dev server (for HMR)
- **http://localhost:8080** — Traefik dashboard (insecure, dev-only)
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
```bash
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:
```bash
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`](../docs/superpowers/specs/2026-04-25-docker-compose-dev-environment-design.md).