LLAB

Docker

The application ships a dockerised stack for both local development and production. The image is role-agnostic — every container runs the same binary, and the compose file is the single source of truth for what each container does.

Topology

Service Image Role
caddy caddy:2-alpine Web server; auto-TLS in prod, plain HTTP in dev
app your image PHP-FPM on :9000
queue same php artisan queue:work
scheduler same php artisan schedule:work (replaces cron)
db postgres:17-alpine Primary datastore
redis redis:8-alpine Cache, session, queue backend

Image layout

docker/Dockerfile is multi-stage:

Stage Purpose
vendor composer install --no-dev for the prod vendor tree
assets npm run build to populate public/build
base PHP-FPM 8.4 + extensions + shared entrypoint
dev Adds Xdebug (off by default), uses host bind mount
prod Bakes app + vendor + assets, optimises autoload, runs as www-data

The shared entrypoint (docker/entrypoint/app.sh) does role-independent bootstrap, optionally caches config/routes and runs migrations (only when RUN_MIGRATIONS=1, which is pinned off for queue/scheduler so workers never race the schema), then execs the compose command.

Local development

cp .env.example .env
cp docker/.env.docker.example docker/.env.docker

make up        # build dev image, start the full stack
make migrate   # first time only

App at http://localhost:8080.

Target Action
make up / make down Start / stop the stack
make sh Shell into the app container
make tinker php artisan tinker inside the container
make migrate / make migrate-fresh Run migrations
make test PHPUnit inside the container
make pint Pint formatter on dirty files

The dev container bind-mounts the project root, so host edits are picked up immediately. Set WWW_USER_ID/WWW_GROUP_ID to your host id -u/id -g to avoid file-ownership issues. Xdebug is opt-in: XDEBUG_MODE=debug,develop make restart.

Production

A multi-arch image can’t be loaded back into the local docker store, so:

Target What it does
make build Multi-arch build into buildx cache (CI validation)
make push Multi-arch build + push — produces the deployable artifact
make build-local Single-arch build loaded locally
APP_IMAGE=ghcr.io/your-org/laravel-api-boilerplate \
  APP_IMAGE_TAG=2026.05.14 make push

The server never builds — docker/docker-compose.prod.yml only references image:. Deploy loop:

git pull
make prod-pull   # docker compose pull
make prod-up     # recreate changed services

Auto-TLS

Caddy reads ${APP_DOMAIN} from docker/.env.prod and provisions a Let’s Encrypt certificate on first boot (point DNS at the server; open ports 80 + 443). Behind an existing TLS proxy? Replace Caddyfile.prod with a plain :80 block.

Migrations

app runs migrate --force on boot when RUN_MIGRATIONS=1 (the default). For zero-downtime, set RUN_MIGRATIONS=0 and run migrations as a one-shot before swapping containers.

Environment files

File Read by Holds
.env Laravel + dev compose App config, mail, API keys
docker/.env.docker Dev compose only UID/GID, ports, dev DB/Redis creds
docker/.env.prod Prod compose + Laravel Everything + image coords, domain, ACME email

Only the *.example siblings are committed; the real files are .gitignored.