Magic Containers on Bunny

Magic Containers are bunny.net's edge container runtime. Your app runs as one pod on bare metal, sandboxed by gVisor, restricted to linux/amd64. Containers in the pod share localhost. The pod autoscales on CPU; there are no persistent volumes for stateless apps, and the pod disk is ephemeral — anything written to it disappears on recycle. Magic Containers runs the HTTP app + the first-run installer only; the queue worker, Reverb, and scheduler are self-hosted always-on processes (compose / k8s / VPS), not CaaS sidecars (ADR 0009). CaaS that scales to zero or idles is the wrong host for a long-lived daemon.

gVisor

gVisor is a user-space kernel sandbox that intercepts a subset of syscalls. Standard PHP apps — including FrankenPHP — run unmodified. Anything that depends on exotic syscalls or raw kernel access will not.

Statelessness doctrine

Because the pod disk is ephemeral, every piece of state moves off the pod:

  • Sessions, cache, and queue table live in Bunny Database (libSQL, database driver). The queue worker is not on the pod — it is a self-hosted always-on process (ADR 0009). On CaaS, set QUEUE_CONNECTION=sync (no worker) or point at a vendor queue (redis / sqs) per Laravel queue docs.
  • User uploads live in Bunny Storage (S3-compatible).
  • Passport signing keys live in env, not in storage/oauth-*.key.

This is why the first-run installer uses a DB lock (installer_locks), not a file: a file lock would not survive a pod recycle, and would not be visible to the next replica.

Bunny Database

Managed libSQL, globally replicated, currently Public Preview and free with a 1 GB per-DB cap. Reach it at libsql://[id].lite.bunnydb.net. Replication has a ~10s window and no read-your-writes on replicas — fine for template-scale apps.

Bunny Storage

S3-compatible object storage. The S3 API flag is set only at zone creation — you cannot retrofit it onto an existing zone. There are no ACLs and no lifecycle rules; set retain_visibility=false so copies do not call GetObjectAcl. Env map: zone nameAWS_ACCESS_KEY_ID + AWS_BUCKET, zone passwordAWS_SECRET_ACCESS_KEY, region codeAWS_DEFAULT_REGION, AWS_ENDPOINT=https://{region}-s3.storage.bunnycdn.com, pull zone → AWS_URL.

CDN and WebSockets

Sticky sessions (a CDN-endpoint option that pins a client to one pod) are not needed when sessions live in Bunny DB — any pod can serve any request. For Reverb, enable CDN WebSockets on the pull zone in front of the pod. It is off until you turn it on, with a default of 500 concurrent connections.

Build and push

docker compose -f docker-compose.build.yml build

The default build target is linux/amd64 — the only arch Magic Containers accept. Push only with your own registry user:

IMAGE_NAME=<registry-user>/qcentic-edge-template:latest \
  docker compose -f docker-compose.build.yml build --push

Architecture

flowchart LR
  Client[Client browser] --> CDN[Bunny CDN pull zone]
  CDN --> Pod[Magic Container pod]
  subgraph Pod[Pod - linux/amd64, gVisor]
    App[FrankenPHP app :8080]
  end
  App --> BunnyDB[(Bunny Database libSQL)]
  App --> BunnyS3[(Bunny Storage S3)]
  SelfHost[Self-hosted VPS / k8s] -.queue / reverb / scheduler.-> BunnyDB
  CDN -.optional CDN WebSockets.-> SelfHost

See also

Built by Qcentic