# Laravel on Docker Compose: naive vs hardened benchmark

Companion artifact for the blog post [Laravel with Docker Compose: 10 Infrastructure Mistakes](/blog/laravel-docker-compose-10-infrastructure-mistakes/).

`bench.sh` creates a fresh `laravel/laravel` app in a temp directory, the way a developer has it locally (`vendor/`, `node_modules/`, `.env`, `.git`). Then it builds the same app two ways and measures the difference.

| Path | What it is |
| --- | --- |
| `naive/Dockerfile` | Single stage on `php:8.5-fpm` (Debian), `COPY . .` with no `.dockerignore`, dev dependencies, Node and Composer in the runtime image, runs as root, migrations plus queue plus scheduler in one `CMD` |
| `naive/compose.yaml` | `DB_HOST: db` when the service is `postgres`, healthcheck on the wrong role, `depends_on` without a condition |
| `hardened/Dockerfile` | Three stages (Composer, Node, runtime) on `php:8.5-fpm-alpine`, `php.ini-production`, tuned OPcache, non-root |
| `hardened/.dockerignore` | Keeps `.env`, `.git`, `vendor/`, `node_modules/` and tests out of the build context |
| `hardened/compose.yaml` | One image, four roles (`migrate`, `app`, `queue`, `scheduler`), `service_healthy` and `service_completed_successfully` gates, read-only root filesystem with tmpfs for `storage/` and `bootstrap/cache/` |
| `hardened/docker/` | Entrypoint that builds config, route and view caches at start, OPcache ini, nginx config |
| `diagram/laravel-compose-topology.drawio` | The hardened topology and its ECS equivalent |

## Run it

Requirements: Docker with BuildKit and Compose v2, `git`, `python3`. About 10 minutes on a laptop. No AWS account needed.

```bash
./bench.sh
# keep the generated Laravel app for poking around:
WORK_DIR=/tmp/laravel-bench ./bench.sh
```

Output goes to `out/results-<UTC timestamp>.csv` (one metric per row) and `out/run-<UTC timestamp>.txt` (full transcript).

What it measures:

- cold build time (`--no-cache`) and rebuild time after a one-line change to `routes/web.php`
- build context size sent to the Docker daemon
- final image size
- what is inside each image: OS package count, `.env`, `.git`, `node_modules`, `vendor/phpunit`, `tests`, build tools, the user it runs as
- the naive Compose file failing on the `DB_HOST` typo and the healthcheck role mismatch (log lines)
- three naive replicas running `migrate --force` at the same time on an empty database (error count)
- requests per second and p50/p95 latency for `GET /` through nginx, with OPcache and Laravel's config/route/view caches toggled, using `ab -n 3000 -c 20` from the `httpd:2.4-alpine` image

Base images are pulled before timing starts, so pull time is not in the build numbers.

## Gotchas we hit

1. **`composer create-project` into a Docker Desktop bind mount failed** with `RecursiveDirectoryIterator::__construct(/w/app/vendor/symfony/http-foundation): Failed to open directory`. The script now creates the project inside the container's `/tmp` and copies it out.
2. **Docker Hub returned `429 Too Many Requests`** on the second run. The script skips the pull when the image is already local. Log in with `docker login` if you hit the anonymous pull limit.
3. **On PHP 8.5 the official image already has OPcache on.** The naive image reports `opcache.enable=1` with no `php.ini` loaded. What it lacks is the production ini, tuned OPcache settings and Laravel's caches, so the benchmark toggles those separately.
4. **Our first hardened build had no `pcntl`.** Neither official PHP image ships it, and without it `queue:work` ignores `--timeout` and dies mid-job on `SIGTERM`. The load test passed anyway. The Dockerfile now installs it and the script prints `ext: pcntl` or `ext: no pcntl` for each image.

## Our run

`out/` holds the CSV and transcript quoted in the blog post (Apple M4, Docker Desktop 29.9.2, Compose v5.6.1, timestamps in the file names). The OS package counts quoted in the post (744 dpkg packages naive, 43 apk packages hardened) came from a `docker run` on the same two images right after that run; the script now prints them too. Your numbers will differ with CPU, disk and network; the ratios are what to compare.
