Setup

Troubleshooting

Common failure modes for a self-hosted server — likely cause and the fix, most frequent first.

Images and attachments show broken (403)

/media is signature-gated and additionally checks a second, httpOnly BEARER cookie — the only way an <img> tag can authenticate, since it can’t carry an Authorization header. That cookie is set SameSite=None; Secure, host-only (no domain override), so it only reaches the browser if the API is served over real HTTPS at its own SERVER_DOMAIN. This breaks when the API is reached over plain HTTP, through a mismatched domain, or when BEHIND_PROXY is on but the proxy isn’t forwarding the original Host header — the request looks insecure to core and the cookie never gets set.

Tip

Confirm the app cookie in devtools: a BEARER cookie should be present for the API’s own domain. If it’s missing, fix the domain/HTTPS setup before looking anywhere else.

No real-time updates (messages only appear on refresh)

The realtime hub (Mercure) is embedded in the app container and rides the API origin — if the browser can’t hold an SSE connection open (a proxy buffering responses, or the hub simply not reachable), messages stop appearing until reload. Check Admin → Health for the realtime-hub probe, and if behind a proxy, make sure it’s SSE-friendly (buffering off, long read timeout).

Voice connects but there’s no audio, or joining fails

Voice signaling (the /rtc WebSocket) rides the API origin and works through a proxy — the media itself is direct WebRTC and doesn’t. See Network configuration for the exact ports (7881/tcp + 50000–50100/udp) and NAT/firewall pitfalls, and Voice setup if LIVEKIT_PUBLIC_URL needs to be set explicitly (self-hosted LiveKit behind NAT). Without those ports open, set VOICE_ENABLED=false for a text-only server.

Search returns nothing

Most often a Meilisearch key mismatch. In ddev development the master key has to match in two places.ddev/.env (the container) and .env.local (Symfony) — missing one boots search with an empty key; in production secrets-init generates and wires it automatically, so this particular mismatch shouldn’t occur there. If the index itself is empty or was lost (e.g. after restoring a database backup), rebuild it:

docker compose exec app php bin/console tyto:search:reindex

Idempotent and safe to run on a live server — see Operations.

Emails not sending

SMTP is configured in the admin panel (database-backed), not environment variables — env-file mail settings have no effect. Open Admin → Settings → Email and use the test mail button to verify the connection. If the test succeeds but real notifications never arrive, check that the worker container (the async job consumer — mail, push, digests) is actually running:

docker compose logs -f worker

Locked out of two-factor authentication

Lost the device and out of recovery codes? Work down this list:

  1. Use a recovery code

    Each one works once — Use a recovery code instead on the login screen. Fastest fix, and needs no admin.

  2. Ask an admin to disable it

    Admin panel → Users → the account’s detail page has a Disable 2FA action (shown once 2FA is on). It deletes the secret and recovery codes so the person can sign in with just their password and set 2FA up again. Audit-logged.

  3. Locked-out sole admin? Use the console

    When the account that needs unlocking is itself the only admin, run the reset from the server instead — also audit-logged: docker compose exec app bin/console tyto:user:reset-2fa admin@example.com.

Registration blocked unexpectedly

If anti-spam registration screening is enabled, a refused signup is usually a StopForumSpam hit above your confidence threshold, not a bug. Add the person to the allowlist (email, domain, IP, or CIDR) or turn ipReputationEnabled off in Admin → Settings → Moderation.

Stuck behind a reverse proxy

WebSocket upgrade, SSE buffering, and the forwarded Host header all need to be right for realtime and voice signaling to work through an existing proxy. Full checklist: Behind a reverse proxy.

The admin setup wizard is gone

It only runs automatically on first login. To reopen it later, go to Admin → Settings and click Run setup wizard (top right of the page).

Container boots but the site shows 502 or a blank page

Usually first-boot certificate issuance: auto-HTTPS needs ports 80/443 reachable from the internet and DNS already pointing at the host before the container starts — Let’s Encrypt can’t issue a certificate otherwise, and the app stays unreachable until it does. Check the logs for the actual error:

docker compose logs
Note

Behind an existing reverse proxy or a VPN-only setup, BEHIND_PROXY=true skips certificate issuance entirely — this symptom shouldn’t apply there.

Still stuck?

If none of the above fits, see Support.