First Boot¶
This guide walks through getting La Suite Meet running for the first time on a fresh server using Docker Compose.
Step 1: Create the project directory¶
Step 2: Generate secrets¶
DJANGO_SECRET=$(openssl rand -hex 32)
LIVEKIT_SECRET=$(openssl rand -hex 32)
DB_PASSWORD=$(openssl rand -hex 16)
KC_DB_PASSWORD=$(openssl rand -hex 16)
KC_CLIENT_SECRET=$(openssl rand -hex 16)
echo "DJANGO_SECRET=$DJANGO_SECRET"
echo "LIVEKIT_SECRET=$LIVEKIT_SECRET"
echo "DB_PASSWORD=$DB_PASSWORD"
echo "KC_DB_PASSWORD=$KC_DB_PASSWORD"
echo "KC_CLIENT_SECRET=$KC_CLIENT_SECRET"
Save these values — you'll need them in the files below.
Step 3: Create the environment file¶
Create .env (replace all <...> placeholders with your values):
# Hosts
MEET_HOST=visio.example.com
KEYCLOAK_HOST=auth.example.com
LIVEKIT_HOST=livekit.example.com
REALM_NAME=meet
# Internal service hostnames (used by nginx-routing.conf)
BACKEND_INTERNAL_HOST=backend
FRONTEND_INTERNAL_HOST=frontend
LIVEKIT_INTERNAL_HOST=livekit
# Django
DJANGO_SETTINGS_MODULE=meet.settings
DJANGO_CONFIGURATION=Production
DJANGO_SECRET_KEY=<DJANGO_SECRET>
DJANGO_ALLOWED_HOSTS=visio.example.com
DJANGO_CSRF_TRUSTED_ORIGINS=https://visio.example.com
PYTHONPATH=/app
# Meet
MEET_BASE_URL=https://visio.example.com
ALLOW_UNREGISTERED_ROOMS=False
# Database
DB_HOST=postgresql
DB_PORT=5432
DB_NAME=meet
DB_USER=meet
DB_PASSWORD=<DB_PASSWORD>
# Redis
REDIS_URL=redis://redis:6379/0
# OIDC (Keycloak)
OIDC_RP_CLIENT_ID=meet
OIDC_RP_CLIENT_SECRET=<KC_CLIENT_SECRET>
OIDC_RP_SIGN_ALGO=RS256
OIDC_RP_SCOPES=openid email
OIDC_OP_AUTHORIZATION_ENDPOINT=https://auth.example.com/realms/meet/protocol/openid-connect/auth
OIDC_OP_TOKEN_ENDPOINT=https://auth.example.com/realms/meet/protocol/openid-connect/token
OIDC_OP_USER_ENDPOINT=https://auth.example.com/realms/meet/protocol/openid-connect/userinfo
OIDC_OP_JWKS_ENDPOINT=https://auth.example.com/realms/meet/protocol/openid-connect/certs
OIDC_OP_LOGOUT_ENDPOINT=https://auth.example.com/realms/meet/protocol/openid-connect/logout
OIDC_REDIRECT_ALLOWED_HOSTS=["https://visio.example.com"]
LOGIN_REDIRECT_URL=https://visio.example.com
LOGIN_REDIRECT_URL_FAILURE=https://visio.example.com
LOGOUT_REDIRECT_URL=https://visio.example.com
# LiveKit
LIVEKIT_API_KEY=meetapikey
LIVEKIT_API_SECRET=<LIVEKIT_SECRET>
LIVEKIT_API_URL=https://livekit.example.com
DJANGO_SETTINGS_MODULE=meet.settingsis required — the app will not start without it.
LIVEKIT_API_URLmust be the public HTTPS URL — it is returned to browser clients as the WebSocket address. Do not use the Docker-internalhttp://livekit:7880.
Step 4: Create the LiveKit configuration¶
Create livekit-server.yaml:
port: 7880
rtc:
tcp_port: 7881
udp_port: 7882
use_external_ip: true # required on cloud servers behind NAT
keys:
meetapikey: <LIVEKIT_SECRET> # must match LIVEKIT_API_SECRET
redis:
address: redis:6379
logging:
level: info
Step 5: Create the nginx routing config¶
The frontend container includes its own nginx that routes API requests to the Django backend. Create nginx-routing.conf:
upstream meet_backend {
server backend:8000 fail_timeout=0;
}
upstream meet_frontend {
server frontend:8080 fail_timeout=0;
}
server {
listen 8083;
server_name localhost;
charset utf-8;
server_tokens off;
client_max_body_size 100M;
location @proxy_to_meet_backend {
proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
proxy_redirect off;
proxy_read_timeout 300s;
proxy_pass http://meet_backend;
}
location @proxy_to_meet_frontend {
proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_redirect off;
proxy_pass http://meet_frontend;
}
location / { try_files $uri @proxy_to_meet_frontend; }
location /api { try_files $uri @proxy_to_meet_backend; }
location /admin { try_files $uri @proxy_to_meet_backend; }
location /static { try_files $uri @proxy_to_meet_backend; }
location /oidc { try_files $uri @proxy_to_meet_backend; }
location /healthz { try_files $uri @proxy_to_meet_backend; }
location /webhook { try_files $uri @proxy_to_meet_backend; }
}
Step 6: Create the Keycloak realm¶
Create keycloak-realm.json:
{
"realm": "meet",
"enabled": true,
"sslRequired": "external",
"clients": [
{
"clientId": "meet",
"enabled": true,
"protocol": "openid-connect",
"publicClient": false,
"secret": "<KC_CLIENT_SECRET>",
"standardFlowEnabled": true,
"redirectUris": ["https://visio.example.com/api/v1.0/callback/"],
"webOrigins": ["https://visio.example.com"]
}
],
"users": [
{
"username": "meet-admin",
"email": "admin@example.com",
"enabled": true,
"emailVerified": true,
"credentials": [{"type": "password", "value": "ChangeMe!", "temporary": true}]
}
]
}
The redirect URI must be
https://visio.example.com/api/v1.0/callback/— Meet uses a versioned API path, not the standard/oidc/callback/.
Step 7: Create the compose file¶
Create compose.yml:
services:
backend:
image: lasuite/meet-backend:latest
restart: unless-stopped
env_file: .env
extra_hosts:
- "auth.example.com:host-gateway"
- "livekit.example.com:host-gateway"
depends_on:
- postgresql
- redis
- livekit
networks:
- internal
frontend:
image: lasuite/meet-frontend:latest
restart: unless-stopped
entrypoint:
- /docker-entrypoint.sh
command: ["nginx", "-g", "daemon off;"]
environment:
- VIRTUAL_HOST=visio.example.com
- VIRTUAL_PORT=8083
- LETSENCRYPT_HOST=visio.example.com
- LETSENCRYPT_EMAIL=you@example.com
env_file: .env
volumes:
- ./nginx-routing.conf:/etc/nginx/conf.d/routing.conf:ro
depends_on:
- backend
networks:
- proxy
- internal
celery:
image: lasuite/meet-backend:latest
restart: unless-stopped
command: ["celery", "-A", "meet.celery_app", "worker", "-l", "INFO"]
env_file: .env
depends_on:
- backend
networks:
- internal
postgresql:
image: postgres:16
restart: unless-stopped
environment:
POSTGRES_DB: meet
POSTGRES_USER: meet
POSTGRES_PASSWORD: <DB_PASSWORD>
volumes:
- meet_db:/var/lib/postgresql/data
networks:
- internal
redis:
image: redis:7
restart: unless-stopped
networks:
- internal
livekit:
image: livekit/livekit-server:latest
restart: unless-stopped
command: --config /config.yaml
environment:
- VIRTUAL_HOST=livekit.example.com
- VIRTUAL_PORT=7880
- LETSENCRYPT_HOST=livekit.example.com
- LETSENCRYPT_EMAIL=you@example.com
ports:
- "7881:7881"
- "7882:7882/udp"
volumes:
- ./livekit-server.yaml:/config.yaml:ro
depends_on:
- redis
networks:
- proxy
- internal
keycloak:
image: quay.io/keycloak/keycloak:20.0.1
restart: unless-stopped
command:
- start-dev
- --import-realm
- --proxy=edge
- --hostname-url=https://auth.example.com
- --hostname-admin-url=https://auth.example.com
- --hostname-strict=false
- --hostname-strict-https=false
environment:
VIRTUAL_HOST: auth.example.com
VIRTUAL_PORT: "8080"
LETSENCRYPT_HOST: auth.example.com
LETSENCRYPT_EMAIL: you@example.com
KEYCLOAK_ADMIN: admin
KEYCLOAK_ADMIN_PASSWORD: <strong-password>
KC_DB: postgres
KC_DB_URL_HOST: kc-postgresql
KC_DB_URL_DATABASE: keycloak
KC_DB_PASSWORD: <KC_DB_PASSWORD>
KC_DB_USERNAME: keycloak
PROXY_ADDRESS_FORWARDING: "true"
volumes:
- ./keycloak-realm.json:/opt/keycloak/data/import/realm.json:ro
depends_on:
- kc-postgresql
networks:
- proxy
- internal
kc-postgresql:
image: postgres:14
restart: unless-stopped
environment:
POSTGRES_DB: keycloak
POSTGRES_USER: keycloak
POSTGRES_PASSWORD: <KC_DB_PASSWORD>
volumes:
- kc_db:/var/lib/postgresql/data
networks:
- internal
volumes:
meet_db:
kc_db:
networks:
proxy:
external: true # nginx-proxy network — must exist before starting this stack
internal:
Step 8: Pull images and start¶
Step 9: Run database migrations¶
All migrations should complete with OK.
Step 10: Verify all containers are running¶
Expected output — all should show Up:
meet-backend-1 Up
meet-frontend-1 Up
meet-celery-1 Up
meet-postgresql-1 Up
meet-redis-1 Up
meet-livekit-1 Up
meet-keycloak-1 Up
meet-kc-postgresql-1 Up
Step 11: DNS and firewall¶
Add three DNS A records pointing to your server:
visio.example.com → <server-public-IP> (Meet)
auth.example.com → <server-public-IP> (Keycloak)
livekit.example.com → <server-public-IP> (LiveKit WebSocket)
Open the LiveKit media ports in your cloud provider's security group:
| Port | Protocol | Notes |
|---|---|---|
| 7881 | TCP | LiveKit TCP media fallback |
| 7882 | UDP | LiveKit RTP/RTCP — critical |
Port 7880 does not need to be open — LiveKit WebSocket runs through nginx-proxy on port 443.
Step 12: First login¶
Once DNS propagates and certificates are issued (usually under a minute):
- Open
https://visio.example.com - You are redirected to
https://auth.example.comfor login - Log in with the user from
keycloak-realm.json(default:meet-admin/ChangeMe!) - Keycloak prompts for a new password on first login
- You are redirected back to Meet and can create your first room
Keycloak admin: https://auth.example.com → admin / your chosen admin password.
Troubleshooting¶
App returns 404 on all routes: DJANGO_SETTINGS_MODULE is missing from .env. Add it and restart.
502 on all routes: The frontend container nginx is not listening on port 8083. Check docker logs meet-frontend-1 for errors. If you see /etc/nginx/conf.d is not writable, ensure nginx-routing.conf is mounted to /etc/nginx/conf.d/routing.conf (not to the templates directory).
Site keeps loading / infinite spinner: The API is returning 301 redirects. Ensure proxy_set_header X-Forwarded-Proto https is present (hardcoded, not $scheme) in the backend location blocks of nginx-routing.conf.
"Invalid parameter: redirect_uri" from Keycloak: The Keycloak client's redirect URI must be https://visio.example.com/api/v1.0/callback/ — not /oidc/callback/. Update via Keycloak admin UI or via 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://visio.example.com/api/v1.0/callback/"],"webOrigins":["https://visio.example.com"]}'
Disconnected from meeting after a few seconds: LiveKit WebSocket is not reachable. Check that LIVEKIT_API_URL=https://livekit.example.com (public URL), the livekit.example.com DNS record exists, and nginx-proxy has issued a TLS cert (curl -s -o /dev/null -w "%{http_code}" https://livekit.example.com/ should return 200).
No audio/video after joining: Firewall is blocking LiveKit media ports. Verify 7881/TCP and 7882/UDP are open in your cloud provider's security group.
docker compose exec fails with "invalid USER value": Docker user namespace remapping is active on this system. Use docker exec -u root <container-name> <command> instead.