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.
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:
- Use a recovery code
Each one works once — Use a recovery code instead on the login screen. Fastest fix, and needs no admin.
- 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.
- 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
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.