Compose production

The production Compose file runs a single published image against a remote libSQL database — Bunny Database, Turso, or your own sqld. This is the self-hosted prod recipe (VPS or your own k8s); Magic Containers runs the HTTP app + installer only, not these daemons (ADR 0009). There is no second build; the same image serves dev and prod. The difference is the env file and the compose file, not the binaries inside the container.

Services

docker-compose.prod.yml defines these services, all derived from one hardened image (read_only: true, non-root uid 1000, no-new-privileges):

Service Role
migrate One-shot. Waits for the remote DB to accept connections, runs php artisan migrate --force, then exits. restart: "no".
app FrankenPHP serving HTTP on :8080. Starts only after migrate completes successfully.
queue php artisan queue:work --tries=3 --timeout=60. Same dependency on migrate.
scheduler php artisan schedule:work. Same dependency on migrate.
reverb php artisan reverb:start on :8081. Default fan-out is in-memory.

app, queue, scheduler, and reverb all share the image and wait on migrate: service_completed_successfully, so schema is current before any of them serve traffic.

Run against a remote libSQL

cp .env.docker.prod.example .env.docker.prod
# set APP_KEY, IMAGE_NAME, DB_URL, DB_AUTH_TOKEN

docker compose -f docker-compose.prod.yml --env-file .env.docker.prod up -d

DB_URL is the libSQL endpoint (libsql://[id].lite.bunnydb.net for Bunny Database, the Turso URL, or your own sqld address). DB_AUTH_TOKEN is the access token the managed provider issues. With both set, the migrate sidecar connects, runs migrations once, and the long-running services follow.

Optional profiles

Two profiles ship disabled by default and opt in with a flag:

  • --profile bundled-db — starts a local ghcr.io/tursodatabase/libsql-server container. Use it to test the prod image on your own machine without a managed DB; leave DB_URL empty and compose defaults it to http://db:8080.
  • --profile redis — starts redis:7-alpine. Needed only when you set REVERB_SCALING_ENABLED=true or run more than one app replica; Laravel has no database persister for Reverb fan-out, so horizontal scale-out requires Redis pub/sub. Leave it off to keep the in-memory server.

One-shot migrate

The migrate service polls DB_URL until the host accepts a TCP/TLS connection, then runs php artisan migrate --force and exits. Because app, queue, scheduler, and reverb declare depends_on: migrate: condition: service_completed_successfully, a failed migration blocks the rest of the stack from starting — the app never serves against a half-migrated schema.

On Magic Containers prefer the web installer (/install) over the compose sidecar: set INSTALLER_ENABLED=true and explicit DB_URL / DB_AUTH_TOKEN, run migrations through the UI, then flip INSTALLER_ENABLED=false and redeploy.

See also

Built by Qcentic