Skip to content

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

mkdir -p ~/docker/meet
cd ~/docker/meet

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.settings is required — the app will not start without it.

LIVEKIT_API_URL must be the public HTTPS URL — it is returned to browser clients as the WebSocket address. Do not use the Docker-internal http://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

docker compose pull
docker compose up -d

Step 9: Run database migrations

docker exec -u root meet-backend-1 python manage.py migrate

All migrations should complete with OK.

Step 10: Verify all containers are running

docker compose ps

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):

  1. Open https://visio.example.com
  2. You are redirected to https://auth.example.com for login
  3. Log in with the user from keycloak-realm.json (default: meet-admin / ChangeMe!)
  4. Keycloak prompts for a new password on first login
  5. You are redirected back to Meet and can create your first room

Keycloak admin: https://auth.example.comadmin / 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.