Common failure modes when running Beacon self-hosted, with the fastest fix for each.
If your problem is not listed, the first three things to check are always:
docker compose logs -f app
docker compose logs -f horizon
docker compose logs -f nginx
First-run setup keeps redirecting to /setup
The setup wizard is gated by a "setup complete" check. The dashboard redirects to /setup until all four steps are finished: workspace, Twilio credentials, business number, API key.
- Reopen
/setupand check which step is missing. - If you closed the browser before the API key step, your team and credentials are saved — pick up at the API key step.
- If the wizard appears to forget state, your
SESSION_DRIVERmay be misconfigured. ConfirmSESSION_DRIVER=databaseand that thesessionstable exists.
"Unauthorized" / missing_team_context from API requests
The API key is missing, malformed, or belongs to a disabled team.
- Check the header is exactly
X-API-Key: beacon_...(orAuthorization: Bearer beacon_...). - Test mode keys must end in
_test. A key without_testwill not bypass licensing. - If you rotated keys in the dashboard, the old one is invalidated immediately.
License key invalid / 402 on every request
Beacon validates every API request against storage/app/beacon.license.
- Confirm the file is present:
docker compose exec app ls -l storage/app/beacon.license - Use a
_test-suffixed API key to confirm Beacon itself is healthy. If_testworks and live keys do not, the license is the problem. - Inspect the validation log in
storage/logs/laravel.log— it names which check failed (signature, expiry, version, fingerprint). - See licensing.md for the full validation matrix.
Twilio webhook returns 401 or nothing arrives
Beacon validates Twilio's X-Twilio-Signature against WEBHOOK_CLIENT_SECRET. A 401 means the signature did not match.
- Confirm
WEBHOOK_CLIENT_SECRETis set to the exact Auth Token of the Twilio account that owns the receiving number. - If you are running through a tunnel, Twilio computes the signature against the public URL. Make sure
APP_URLand the URL you registered with Twilio match the tunnel host (no trailing slash, correct scheme). - Inspect
webhook_callsto see if Twilio is even reaching you:docker compose exec app php artisan tinker --execute 'Spatie\WebhookClient\Models\WebhookCall::latest()->limit(5)->get()->toArray();' - See webhooks.md for full setup.
Horizon dashboard is empty / shows no jobs
- Confirm the Horizon container is running:
docker compose ps horizon. - Confirm Redis is healthy:
docker compose exec redis redis-cli pingshould returnPONG. - Hit
/horizonin a browser. If you get 403, Horizon's view gate is rejecting the current user — log in as a user the gate allows.
Inbound messages never appear in threads
Walk the path:
- Did Twilio call your webhook? Check
webhook_calls. - Did Beacon accept it? A row in
webhook_callswith no error means yes. - Did Horizon pick up the job?
docker compose logs -f horizonand look forProcessInboundSmsJob. - Did the job match a business number? Inbound is rejected if the destination number is not a registered business number on any team.
"Vite manifest not found" in the browser
On the Docker path, frontend assets are baked into ghcr.io/apxcde/beacon. After docker compose pull, recreate the code volume so Nginx serves the new hashed assets:
docker compose down && docker volume rm beacon_beacon_code && docker compose up -d
If you installed via Composer from packages.apexcode.dev and are not using the image's baked assets:
npm install
npm run build
SQLite database locked / write errors
SQLite is fine for one container, fragile under concurrent writers. If you are running Horizon, scheduler, and the app container against the same SQLite file under load, switch to MySQL:
docker compose --profile mysql up -d
See self-hosted-docker.md for the full env block.
OTP verification always fails
- OTPs expire (default 5 minutes). Re-check
config/services.phpor your env override. - The verification request must reuse the same business number as the generation request — OTPs are scoped to the sending number.
- Test mode (
_testkeys) generates predictable codes — check the OTP generate/verify responses and logs if your tests are flaky.
"License expired" warning in logs but requests still work
Beacon honors a 14-day grace period after expiry by default (LICENSE_GRACE_PERIOD_DAYS). You will keep serving requests but should renew. Past the grace period, the middleware returns 402.
Rate limit errors (429 Too Many Requests)
Default token bucket: 10 req/sec, burst 50. Adjustable via env vars (search config/services.php and the rate.limit middleware). For a busy single-tenant install, raise the bucket; for a multi-tenant install, the per-team limits prevent one team from starving others.
Where to look next
- Full env reference: configuration.md
- API errors: api.md
- MCP errors: mcp.md
- Webhook flow: webhooks.md
- Licensing flow: licensing.md