Troubleshooting¶
Diagnostic checklist¶
Before investigating specific symptoms, collect basic information:
# Are all containers running?
docker compose ps
# Recent logs per service
docker compose logs --tail=50 backend
docker compose logs --tail=50 frontend
docker compose logs --tail=50 livekit
docker compose logs --tail=50 keycloak
Application issues¶
App returns 404 on all routes¶
Cause: DJANGO_SETTINGS_MODULE is not set in .env.
Fix: Add DJANGO_SETTINGS_MODULE=meet.settings to .env and restart.
502 Bad Gateway on all routes¶
Cause: The frontend container nginx is not listening on port 8083, or the backend is unreachable.
Steps:
1. Check docker compose logs frontend for nginx errors
2. Verify nginx-routing.conf is mounted to /etc/nginx/conf.d/routing.conf:ro in the frontend container
3. Verify the backend is running: docker compose ps backend
API returns 301 in a loop / site keeps loading¶
Cause: Django's SECURE_SSL_REDIRECT is active and the nginx routing config is forwarding HTTP instead of HTTPS to the backend.
Fix: In the nginx routing template, the backend proxy location must hardcode the proto header:
Then restart the frontend container.Login redirects but loops back to login page¶
Cause A: OIDC redirect URI mismatch.
Meet's callback URL is https://meet.example.com/api/v1.0/callback/, not the standard /oidc/callback/. Update your OIDC provider's client configuration to use the correct URI (including the trailing slash).
Cause B: Backend cannot reach Keycloak for token exchange.
The backend must be on the proxy Docker network to resolve public hostnames like auth.example.com. Check your compose file: the backend service should list both proxy and internal in its networks:.
Cause C: Session cookie mismatch. DJANGO_CSRF_TRUSTED_ORIGINS does not include your full HTTPS domain.
"Invalid parameter: redirect_uri" from Keycloak¶
The redirect URI sent by Meet does not match what's registered in Keycloak. Update it via the Keycloak admin API:
KC_IP=$(docker inspect meet-keycloak-1 --format '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' | tr ' ' '\n' | tail -1)
TOKEN=$(curl -s -X POST "http://$KC_IP:8080/realms/master/protocol/openid-connect/token" \
-d "client_id=admin-cli&username=admin&password=<KC_ADMIN_PASSWORD>&grant_type=password" \
| python3 -c "import sys,json; print(json.load(sys.stdin)['access_token'])")
UUID=$(curl -s "http://$KC_IP:8080/admin/realms/meet/clients?clientId=meet" \
-H "Authorization: Bearer $TOKEN" \
| python3 -c "import sys,json; print(json.load(sys.stdin)[0]['id'])")
curl -s -X PUT "http://$KC_IP:8080/admin/realms/meet/clients/$UUID" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"redirectUris":["https://meet.example.com/api/v1.0/callback/"],"webOrigins":["https://meet.example.com"]}'
Database migration errors¶
# Check migration status
docker compose exec backend python manage.py showmigrations
# Run pending migrations
docker compose exec backend python manage.py migrate
If docker compose exec fails with "invalid USER value" (Docker user namespace remapping), use docker exec -u root instead (see Container issues).
Audio/Video issues¶
Disconnected from meeting immediately / "You left the meeting"¶
Most likely cause: LiveKit WebSocket is unreachable from the browser.
The browser connects to the URL returned by GET /api/v1.0/rooms/{id}/token/. If LIVEKIT_API_URL is set to the Docker-internal address (http://livekit:7880), browsers receive that address and can't connect.
Fix: LIVEKIT_API_URL must be the public HTTPS URL:
Verify the LiveKit subdomain is accessible:
Can join the room but no audio/video¶
Cause: UDP port 7882 is blocked.
LiveKit falls back to TCP (port 7881) when UDP is unavailable, which increases latency significantly. Verify: 1. Port 7882/UDP is open in your cloud provider's firewall (security group / network rules) 2. Port 7881/TCP is also open for TCP fallback
Audio works but video is very pixelated or choppy¶
Cause: Network congestion or insufficient server bandwidth.
LiveKit's simulcast should adapt automatically. If it doesn't: 1. Check server bandwidth; LiveKit forwards all streams through the server 2. Have participants check their own connection speed 3. Review LiveKit metrics (see Monitoring)
Recording issues¶
minio-init exited with an error / bucket not created¶
The minio-init container runs once at startup and exits. If it failed (check with docker compose logs minio-init), run the bucket initialization manually using a temporary mc container:
docker run --rm --network <project>_internal --entrypoint /bin/sh minio/mc:latest -c \
"mc alias set myminio http://minio:9000 minioadmin <MINIO_PASSWORD> && \
mc mb myminio/meet-media-storage --ignore-existing && \
mc anonymous set download myminio/meet-media-storage"
Replace <project> with your Docker Compose project name (typically the directory name) and <MINIO_PASSWORD> with the value of MINIO_ROOT_PASSWORD in your compose.yml.
Recording button is missing¶
Recording requires explicit activation and S3 storage to be configured. Ensure the following env vars are set:
- RECORDING_ENABLE=True
- AWS_S3_ENDPOINT_URL
- AWS_S3_ACCESS_KEY_ID
- AWS_S3_SECRET_ACCESS_KEY
- AWS_STORAGE_BUCKET_NAME
Verify via the API: curl https://meet.example.com/api/v1.0/config/ | jq .recording.is_enabled should return true.
Recording button clicked but UI stays on "starting"¶
Cause: The backend never receives the LiveKit webhook that signals recording has started or stopped. This is almost always a DJANGO_ALLOWED_HOSTS issue combined with Django's HTTP-to-HTTPS redirect.
When LiveKit sends webhooks to http://backend:8000/... (internal Docker URL), Django's SecurityMiddleware redirects HTTP to HTTPS, and the request fails. When the Host header is not in ALLOWED_HOSTS, Django returns 400.
Fix:
-
In your
.env, ensureDJANGO_ALLOWED_HOSTSincludes the internal service names: -
In
livekit-server.yaml, configure the webhook to use the public HTTPS URL, not the internal Docker address: -
In
compose.yml, add thelivekitservice to theproxynetwork so it can resolve the public domain name internally. -
Recreate the affected containers:
Check that webhooks are now arriving with docker compose logs backend | grep webhooks-livekit.
Recording fails immediately / "no response from servers"¶
Cause: LiveKit Egress refuses to start because the server reports fewer available CPUs than the default room_composite_cpu_cost of 4.0. This is common on VPS and shared hosting.
Fix: Add CPU cost overrides to livekit-egress.yaml:
docker compose restart livekit-egress
Recording starts but file never appears¶
- Check Egress logs:
docker compose logs livekit-egress - Check MinIO (list objects in bucket):
- Check webhook delivery:
docker compose logs backend | grep storage-hook
Recording download link shows "Verify your meeting code"¶
Cause A: RECORDING_DOWNLOAD_BASE_URL is set to the bare domain instead of including the /recording path.
The email notification link is constructed as {RECORDING_DOWNLOAD_BASE_URL}/{recording-id}. The frontend route for recording pages is /recording/<uuid>, not /<uuid>. Using the bare domain sends users to a page that interprets the UUID as a room code.
Fix: Set RECORDING_DOWNLOAD_BASE_URL with the /recording path:
docker compose up -d --force-recreate backend celery
Cause B: The /media/ location is missing from the nginx routing template.
The download button on the recording page links to https://meet.example.com/media/recordings/<uuid>.mp4. Without a routing rule, this falls through to the React frontend which shows "Verify your meeting code".
Fix: Add a MinIO proxy to the template; see Reverse Proxy & Routing for the full config with recording support. Restart the frontend after updating: docker compose restart frontend
Traefik issues¶
Traefik refuses to start¶
Cause: acme.json has wrong permissions or does not exist.
Fix:
touch ~/docker/traefik/acme.json
chmod 600 ~/docker/traefik/acme.json
docker compose restart traefik
Service returns 502 / Traefik can't reach a container¶
Cause: The container is not on the proxy Docker network, which Traefik uses for backend connections.
Fix: Ensure the service has proxy in its networks: list:
docker compose up -d --force-recreate <service>
Route not appearing / service returns 404¶
Cause: traefik.enable=true label is missing.
Since --providers.docker.exposedByDefault=false is set, containers must explicitly opt in. Add the label and recreate the container:
TLS certificate not being issued¶
- Verify port 80 is publicly reachable (required for the HTTP-01 ACME challenge)
- Verify DNS resolves to this server before Traefik starts
- Check certificate logs:
docker logs traefik | grep -i "acme\|certificate\|error"
Port conflict on 80 or 443¶
Another process (nginx-proxy, Apache, etc.) is already bound to the port. Stop it before starting Traefik:
Docker and container issues¶
docker compose exec fails with "invalid USER value"¶
Cause: Docker user namespace remapping (userns-remap) is enabled in /etc/docker/daemon.json on this host. This feature maps container UIDs to an unprivileged range on the host for security isolation. A side effect is that docker compose exec cannot resolve the container's internal user against the remapped UID space and fails with invalid USER value.
Fix: Use docker exec -u root directly, bypassing the user lookup:
# Instead of:
docker compose exec backend python manage.py migrate
# Use:
docker exec -u root meet-backend-1 python manage.py migrate
This applies to any docker compose exec command: migrations, Django management commands, shell access, etc. The container name follows the pattern <project>-<service>-1 (e.g., meet-backend-1 if your project directory is meet).
Container keeps restarting¶
Common causes:
- Missing required environment variable (check for ImproperlyConfigured errors)
- Port already in use on the host
- Permission error on a mounted volume
Out of disk space¶
df -h
# Clean unused Docker resources
docker system prune -f
docker volume prune -f # WARNING: removes unused volumes
Getting more help¶
- Search existing issues: github.com/suitenumerique/meet/issues
- Matrix community: #meet-official:matrix.org
- Open a new issue: include your Meet version, deployment method, and relevant logs