Environment variables
Env is the contract between the image, CI, and Bunny. The same image runs dev, CI, and prod — only the env file changes. Set these in .env.docker.dev / .env.docker.prod; never bake secrets into the image.
Always set
| Var | Value |
|---|---|
APP_NAME |
Public product name (Filament brandName, mail from-name, HTML title). PHP fallback is "Qcentic Edge Template". |
APP_KEY |
Stable forever. Generate: php -r "echo 'base64:'.base64_encode(random_bytes(32)).PHP_EOL;" |
APP_URL |
App origin, no trailing slash. |
APP_ENV / APP_DEBUG |
production / false on Magic Containers. |
Database (libSQL)
| Var | Value |
|---|---|
DB_CONNECTION |
libsql |
DB_URL |
libsql://[id].lite.bunnydb.net (or Turso / self-hosted sqld). Empty in dev defaults to the local sqld sidecar. |
DB_AUTH_TOKEN |
Required for remote libSQL. |
Session, cache, and queue all use the database driver on libSQL — zero extra services.
Object storage (Bunny S3 map)
Bunny Storage speaks the S3 API. Map the zone fields precisely:
| Bunny field | Env var |
|---|---|
| Zone name | AWS_ACCESS_KEY_ID + AWS_BUCKET |
| Zone password | AWS_SECRET_ACCESS_KEY |
Region code (de / ny / …) |
AWS_DEFAULT_REGION (never a URL) |
| S3 API host | AWS_ENDPOINT=https://{region}-s3.storage.bunnycdn.com |
| Pull zone hostname | AWS_URL |
AWS_ENDPOINT is the S3 API host; AWS_URL is the public/CDN base. Storage::url() uses AWS_URL; Storage::temporaryUrl() signs against AWS_ENDPOINT. Set retain_visibility=false on the disk — Bunny has no object ACLs, so copies must not call GetObjectAcl.
Assets
| Var | Value |
|---|---|
ASSET_URL |
Prefixes asset() / Vite build URLs for a static-asset CDN. Empty = app origin. |
Applied at PHP runtime, so one image serves many CDN origins. Do not set Vite base to a CDN in vite.config.js — that bakes the origin into the build.
Livewire
| Var | Value |
|---|---|
LIVEWIRE_TEMPORARY_FILE_UPLOAD_DISK |
s3 on multi-replica Magic Containers; local is OK in single-container compose. |
On multiple pods the upload request and the form submit can land on different replicas — only the shared S3 disk is visible to both.
Passport / API
| Var | Value |
|---|---|
PASSPORT_PRIVATE_KEY / PASSPORT_PUBLIC_KEY |
PEM in env, one-line \n-escaped for Magic Containers. Same class of secret as APP_KEY. |
API_KEY |
Content API Bearer key, if the Content API plugin is enabled. |
Generate with php artisan passport:keys, paste the \n-escaped PEM, then delete storage/oauth-*.key — Magic Containers have no persistent disk.
Realtime
| Var | Value |
|---|---|
BROADCAST_CONNECTION |
reverb (default) points at a Reverb process or remote Reverb host. pusher = Pusher Cloud (clone option). log = broadcasts off. |
REVERB_HOST |
Reverb hostname clients connect to. |
REVERB_SCALING_ENABLED |
Off by default (in-memory fan-out). On only with Redis, typically when replica count > 1. |
CDN WebSockets (Bunny pull-zone feature) stay off until enabled — it sits in front of Reverb, not instead of it. On a CaaS HTTP-only deploy (Magic Containers, Cloud Run, Cloudflare Containers/Workers) there is no Reverb in the pod: either run Reverb on a self-hosted host and point REVERB_HOST, or set BROADCAST_CONNECTION=pusher (Pusher Cloud) or log (off). See Laravel broadcasting docs.
Queue
| Var | Value |
|---|---|
QUEUE_CONNECTION |
database (default, libSQL) on self-hosted compose/k8s/VPS — a queue:work daemon must run there. sync = no worker, jobs run inline. redis / sqs / … via Laravel queue docs. |
The queue worker is a self-hosted always-on process (ADR 0009), not a CaaS sidecar. On a CaaS HTTP-only deploy, leave QUEUE_CONNECTION=sync (no daemon) or point at a vendor queue (redis / sqs) per Laravel queue docs.
Installer
| Var | Value |
|---|---|
INSTALLER_ENABLED |
true on first deploy; false after the DB lock is set. |
The env var opens the app; the installer_locks row records that setup ran. See the first-run installer page for the two-gate flow.
See also: