# ============================================================ # Matrix Self-Hosted Stack # Services: Continuwuity, Coturn, LiveKit, lk-jwt-service # # Domains (replace throughout): # matrix.example.com — homeserver # livekit.example.com — LiveKit + JWT service # # Before starting: # 1. Fill in all YOUR_* placeholders # 2. Set your email for Let's Encrypt # 3. Run: docker network create proxy # 4. Then: docker compose up -d # 5. Grab admin password: docker compose logs homeserver | grep "Created user" # 6. Remove the --execute flag from homeserver command after first boot # ============================================================ services: # ---------------------------------------------------------- # Traefik — Reverse Proxy & TLS termination # Handles HTTPS for matrix.example.com and livekit.example.com # Does NOT handle coturn (raw UDP/TCP, not HTTP) # ---------------------------------------------------------- traefik: image: traefik:latest container_name: traefik restart: unless-stopped ports: - "80:80" - "443:443" volumes: - /var/run/docker.sock:/var/run/docker.sock:ro - acme:/etc/traefik/acme environment: TRAEFIK_LOG_LEVEL: INFO TRAEFIK_ENTRYPOINTS_WEB_ADDRESS: ":80" TRAEFIK_ENTRYPOINTS_WEB_HTTP_REDIRECTIONS_ENTRYPOINT_TO: websecure TRAEFIK_ENTRYPOINTS_WEBSECURE_ADDRESS: ":443" TRAEFIK_ENTRYPOINTS_WEBSECURE_HTTP_TLS_CERTRESOLVER: letsencrypt # Allow encoded characters needed by Matrix TRAEFIK_ENTRYPOINTS_WEBSECURE_HTTP_ENCODEDCHARACTERS_ALLOWENCODEDSLASH: "true" TRAEFIK_ENTRYPOINTS_WEBSECURE_HTTP_ENCODEDCHARACTERS_ALLOWENCODEDHASH: "true" TRAEFIK_CERTIFICATESRESOLVERS_LETSENCRYPT: "true" TRAEFIK_CERTIFICATESRESOLVERS_LETSENCRYPT_ACME_EMAIL: "you@example.com" # EDIT THIS TRAEFIK_CERTIFICATESRESOLVERS_LETSENCRYPT_ACME_KEYTYPE: EC384 TRAEFIK_CERTIFICATESRESOLVERS_LETSENCRYPT_ACME_HTTPCHALLENGE: "true" TRAEFIK_CERTIFICATESRESOLVERS_LETSENCRYPT_ACME_HTTPCHALLENGE_ENTRYPOINT: web TRAEFIK_CERTIFICATESRESOLVERS_LETSENCRYPT_ACME_STORAGE: /etc/traefik/acme/acme.json TRAEFIK_PROVIDERS_DOCKER: "true" TRAEFIK_PROVIDERS_DOCKER_EXPOSEDBYDEFAULT: "false" networks: - proxy # ---------------------------------------------------------- # Continuwuity — Matrix Homeserver # ---------------------------------------------------------- homeserver: image: forgejo.ellis.link/continuwuation/continuwuity:latest container_name: continuwuity restart: unless-stopped # Remove the --execute flag after first boot once you have your admin password command: /sbin/conduwuit --execute "users create-user admin" depends_on: - traefik volumes: - db:/var/lib/continuwuity - ./continuwuity.toml:/etc/continuwuity.toml:ro # Use host DNS to avoid Docker's resolver causing federation issues - /etc/resolv.conf:/etc/resolv.conf:ro networks: - proxy labels: - "traefik.enable=true" # Serve matrix.example.com AND well-known paths on example.com for delegation - "traefik.http.routers.continuwuity.rule=(Host(`matrix.example.com`) || (Host(`example.com`) && PathPrefix(`/.well-known/matrix`)))" - "traefik.http.routers.continuwuity.entrypoints=websecure" - "traefik.http.routers.continuwuity.tls.certresolver=letsencrypt" - "traefik.http.services.continuwuity.loadbalancer.server.port=6167" environment: CONTINUWUITY_SERVER_NAME: "example.com" # Your Matrix ID domain (@user:example.com) CONTINUWUITY_DATABASE_PATH: /var/lib/continuwuity CONTINUWUITY_ADDRESS: 0.0.0.0 CONTINUWUITY_PORT: 6167 CONTINUWUITY_MAX_REQUEST_SIZE: "20000000" # ~20 MB CONTINUWUITY_ALLOW_REGISTRATION: "false" # Enable with a token for invite-only CONTINUWUITY_REGISTRATION_TOKEN: "YOUR_INVITE_TOKEN" # EDIT THIS CONTINUWUITY_ALLOW_FEDERATION: "true" CONTINUWUITY_ALLOW_CHECK_FOR_UPDATES: "true" CONTINUWUITY_TRUSTED_SERVERS: '["matrix.org"]' CONTINUWUITY_CONFIG: /etc/continuwuity.toml # Well-known delegation so clients and servers find your homeserver CONTINUWUITY_WELL_KNOWN: | { client=https://matrix.example.com, server=matrix.example.com:443 } ulimits: nofile: soft: 1048567 hard: 1048567 # ---------------------------------------------------------- # Coturn — TURN/STUN server for legacy 1:1 calls # Uses host networking — Traefik does NOT proxy this. # Reachable directly on the host IP via UDP/TCP ports 3478/5349 # and the relay port range 50201-65535. # ---------------------------------------------------------- coturn: image: coturn/coturn:latest container_name: coturn restart: unless-stopped network_mode: host volumes: - ./coturn.conf:/etc/coturn/turnserver.conf:ro # ---------------------------------------------------------- # LiveKit — Media server for MatrixRTC / Element Call # Also uses host networking for RTC performance. # Traefik proxies HTTP/WS on port 7880 via livekit.example.com. # ---------------------------------------------------------- livekit: image: livekit/livekit-server:latest container_name: livekit restart: unless-stopped command: --config /etc/livekit.yaml network_mode: host volumes: - ./livekit.yaml:/etc/livekit.yaml:ro # Note: with network_mode: host, Traefik cannot directly label this container. # The livekit.example.com routing is handled via the lk-jwt-service container # labels, with LiveKit itself accessed at 127.0.0.1:7880 from that container's # perspective. See lk-jwt-service labels below. # ---------------------------------------------------------- # lk-jwt-service — Issues JWT tokens for LiveKit # Matrix users authenticate here before joining calls. # Traefik routes livekit.example.com here, with path-based # splitting: /sfu/get, /healthz, /get_token go to this service; # everything else is reverse-proxied onward to LiveKit on :7880. # ---------------------------------------------------------- lk-jwt-service: image: ghcr.io/element-hq/lk-jwt-service:latest container_name: lk-jwt-service restart: unless-stopped environment: - LIVEKIT_JWT_BIND=:8081 - LIVEKIT_URL=wss://livekit.example.com # EDIT: your LiveKit domain - LIVEKIT_KEY=YOUR_LK_KEY # EDIT: from generate-keys - LIVEKIT_SECRET=YOUR_LK_SECRET # EDIT: from generate-keys - LIVEKIT_FULL_ACCESS_HOMESERVERS=example.com # EDIT: your Matrix domain ports: - "127.0.0.1:8081:8081" # Only bind to localhost; Traefik reaches it here networks: - proxy labels: - "traefik.enable=true" # High-priority rule: JWT paths go to lk-jwt-service - "traefik.http.routers.livekit-jwt.rule=Host(`livekit.example.com`) && (PathPrefix(`/sfu/get`) || PathPrefix(`/healthz`) || PathPrefix(`/get_token`))" - "traefik.http.routers.livekit-jwt.entrypoints=websecure" - "traefik.http.routers.livekit-jwt.tls.certresolver=letsencrypt" - "traefik.http.routers.livekit-jwt.priority=10" - "traefik.http.services.livekit-jwt.loadbalancer.server.port=8081" # Low-priority rule: everything else on livekit.example.com goes to LiveKit itself - "traefik.http.routers.livekit.rule=Host(`livekit.example.com`)" - "traefik.http.routers.livekit.entrypoints=websecure" - "traefik.http.routers.livekit.tls.certresolver=letsencrypt" - "traefik.http.routers.livekit.priority=1" - "traefik.http.services.livekit.loadbalancer.server.url=http://127.0.0.1:7880" # ---------------------------------------------------------- # Volumes & Networks # ---------------------------------------------------------- volumes: db: acme: networks: proxy: external: true