Makefile so you don’t need to remember raw docker compose invocations. This is the same setup used in the Quickstart — this page covers the rest of the day-to-day commands.
Services
app— the Laravel application, serving on port8000. The image includes PCOV, so PHP test coverage works without extra setup.vite— the frontend dev server, serving assets and HMR on port5173.queue— runsphp artisan queue:work --tries=3against the same image asapp. Required for email notifications to actually be delivered — nothing else in the stack processes queued jobs.nsfwjs— the image-moderation service used to screen avatar uploads, on port3333.appdepends on it;queuedepends onapp.uptime-kuma— an optional monitoring dashboard on port3001, gated behind the Composemonitoringprofile so it doesn’t start with the rest of the stack by default (see below).
.env.docker file — docker-compose.yml declares each service’s environment as plain KEY: ${KEY} interpolation from whatever environment the docker compose process itself runs in. See Configuration for what each variable does, and below for where those values actually come from.
Where configuration comes from: Doppler or .env
Every make target that needs environment variables runs an ensure-env step first, and the Makefile picks one of two modes automatically by checking whether this directory has ever been linked to a Doppler project (doppler setup):
- Linked to Doppler (the core team) — the
Makefilewraps every command indoppler run --, injecting your Doppler config’s secrets straight into thedocker composeprocess. Doppler is the sole source of truth; no.envfile is read or written, andensure-envis a no-op. - Not linked to Doppler (contributors, forks, anyone without Doppler access) —
ensure-envfalls back to a plain.envfile at the repo root: it copies.env.exampleto.envon first run and generates a freshAPP_KEYif one isn’t set yet, both idempotently.docker composethen reads that.envautomatically for${KEY}interpolation.
If you’re on the Doppler-linked path, generate Seeing
APP_KEY yourself before your first make setup/make up — nothing inside the container writes back to Doppler:MissingAppKeyException or another config-looking error? In Doppler mode, diff doppler secrets --only-names against the environment: blocks in docker-compose.yml. In fallback mode, confirm .env exists with a non-empty APP_KEY=base64:... line — delete it and re-run any make target to regenerate it from scratch.php artisan serve normally treats the mere presence of a .env file as a signal to filter which environment variables reach the request-handling process. That’s harmless in the fallback mode (the filtered set is the same .env it read from), but it would break the Doppler-linked mode, where the real config lives only in the process environment injected by doppler run --, not in a file. That’s why the app container’s CMD runs serve with --no-reload — it disables that filtering so the full environment always passes through, regardless of which mode is active.Common commands
Monitoring stack
uptime-kuma doesn’t start with make up or make dev — it lives behind Compose’s monitoring profile so it’s opt-in:
Running tests in Docker
Troubleshooting: stale Vite HMR
If hot reload stops working and the browser console shows WebSocket errors, the file watcher inside thevite container has likely desynced from the filesystem — this is more common when the repo lives on non-native storage (for example, an external or removable drive), since Vite falls back to polling (server.watch.usePolling in vite.config.js) rather than native filesystem events across Docker bind mounts.
1
Restart the containers
2
If that doesn't fix it, rebuild from scratch
This isn’t fixed by raising the
fs.inotify.max_user_watches kernel limit — that sysctl is global, not per-container, so Docker won’t start a container that tries to set it per-service. Polling is the mechanism actually in effect here, and a restart forces a fresh full re-scan.