services: postgres: image: postgres:16 container_name: webinar-postgres restart: unless-stopped environment: POSTGRES_DB: ${DB_NAME} POSTGRES_USER: ${DB_USER} POSTGRES_PASSWORD: ${DB_PASSWORD} volumes: - postgres-data:/var/lib/postgresql/data ports: - "${DB_PORT_EXPOSED:-5432}:5432" healthcheck: test: ["CMD-SHELL", "pg_isready -U ${DB_USER} -d ${DB_NAME}"] interval: 5s timeout: 5s retries: 10 minio: image: minio/minio:RELEASE.2024-12-18T13-15-44Z container_name: webinar-minio restart: unless-stopped command: server /data --console-address ":9001" environment: MINIO_ROOT_USER: ${MINIO_ACCESS_KEY} MINIO_ROOT_PASSWORD: ${MINIO_SECRET_KEY} volumes: - minio-data:/data ports: - "${MINIO_PORT_EXPOSED:-9000}:9000" - "${MINIO_CONSOLE_PORT_EXPOSED:-9001}:9001" healthcheck: test: ["CMD", "mc", "ready", "local"] interval: 5s timeout: 5s retries: 10 backend: build: context: ./backend container_name: webinar-backend restart: unless-stopped depends_on: postgres: condition: service_healthy minio: condition: service_healthy environment: SERVER_PORT: 8080 DB_HOST: postgres DB_PORT: 5432 DB_NAME: ${DB_NAME} DB_USER: ${DB_USER} DB_PASSWORD: ${DB_PASSWORD} JWT_SECRET: ${JWT_SECRET} JWT_EXPIRATION_MINUTES: ${JWT_EXPIRATION_MINUTES} ADMIN_EMAIL: ${ADMIN_EMAIL} ADMIN_PASSWORD: ${ADMIN_PASSWORD} SWAGGER_ENABLED: ${SWAGGER_ENABLED} MINIO_ENDPOINT: http://minio:9000 MINIO_ACCESS_KEY: ${MINIO_ACCESS_KEY} MINIO_SECRET_KEY: ${MINIO_SECRET_KEY} MINIO_BUCKET: ${MINIO_BUCKET} MAIL_HOST: ${MAIL_HOST} MAIL_PORT: ${MAIL_PORT} MAIL_USER: ${MAIL_USER} MAIL_PASSWORD: ${MAIL_PASSWORD} MAIL_FROM: ${MAIL_FROM} MAIL_SMTP_AUTH: ${MAIL_SMTP_AUTH} MAIL_SMTP_STARTTLS: ${MAIL_SMTP_STARTTLS} VIDEO_PROVIDER: ${VIDEO_PROVIDER} # Adres serwera LiveKit widziany przez przegladarke uczestnika. # Konfiguracja kluczy idzie przez panel, tak samo jak SMTP. LIVEKIT_URL: ${LIVEKIT_URL:-} CORS_ALLOWED_ORIGINS: ${CORS_ALLOWED_ORIGINS} LOG_LEVEL_ROOT: ${LOG_LEVEL_ROOT} LOG_LEVEL_APP: ${LOG_LEVEL_APP} ports: - "${BACKEND_PORT_EXPOSED:-8080}:8080" frontend: build: context: ./frontend container_name: webinar-frontend restart: unless-stopped depends_on: - backend ports: - "${FRONTEND_PORT_EXPOSED:-8081}:80" # --- profil livekit: wlasny serwer wideo --- # # DLACZEGO PROFIL, A NIE ZWYKLE USLUGI # ------------------------------------------------------------------------- # Instancja w trybie NONE albo na Jitsi nie potrzebuje serwera wideo, a te # dwa kontenery zajmuja pamiec i otwieraja porty. Profil sprawia, ze # `docker compose up` ich nie dotyka: uruchamiaja sie wylacznie na zadanie, # poleceniem `docker compose --profile livekit up -d`. livekit: # Wersja serwera musi nadazac za `livekit-client` we froncie. # # Klient 2.22 laczy sie sciezka `/rtc/v1`, ktorej serwer 1.8 nie zna: # przegladarka dostaje 404 i komunikat "v1 RTC path not found. Consider # upgrading your LiveKit server version". Sala wyglada wtedy na dzialajaca # (uczestnik wchodzi, widzi liste obecnych), ale nie plynie ani obraz, # ani dzwiek. Podnoszac `livekit-client`, sprawdz wersje serwera. image: livekit/livekit-server:v1.13 container_name: webinar-livekit profiles: ["livekit"] restart: unless-stopped command: --config /etc/livekit.yaml volumes: - ./livekit/livekit.yaml:/etc/livekit.yaml:ro environment: LIVEKIT_KEYS: "${LIVEKIT_API_KEY}: ${LIVEKIT_API_SECRET}" # Zakres portow UDP. Ma pierwszenstwo przed wartoscia w livekit.yaml, # bo serwer nie rozwija zmiennych w pliku konfiguracyjnym. Instancja, # ktora musi ominac porty zajete w systemie (na Windows sa rezerwacje # Hyper-V), zmienia wylacznie LIVEKIT_UDP_RANGE w .env. UDP_PORT: ${LIVEKIT_UDP_RANGE:-50000-50100} # Adres rozglaszany uczestnikom. Przy uruchomieniu lokalnym ustaw # NODE_IP=127.0.0.1 w .env: bez tego serwer rozglasza publiczny adres # lacza, a przegladarka na tej samej maszynie nie zestawi polaczenia. NODE_IP: ${LIVEKIT_NODE_IP:-} ports: # sygnalizacja WebSocket i API serwera - "${LIVEKIT_PORT:-7880}:7880" # RTC po TCP: droga zapasowa, gdy UDP jest zablokowane w sieci klienta - "${LIVEKIT_TCP_PORT:-7881}:7881" # media po UDP. To jest **glowna** droga ruchu audio i wideo: # bez tego zakresu uczestnicy widza sie, ale sie nie slysza # Ten sam zakres po obu stronach: UDP_PORT ustawia go wewnatrz # kontenera, wiec mapowanie musi go odwzorowac jeden do jednego. # Rozjechanie stron konczy sie bledem "invalid ranges specified". - "${LIVEKIT_UDP_RANGE:-50000-50100}:${LIVEKIT_UDP_RANGE:-50000-50100}/udp" # TURN: przekazuje ruch, gdy polaczenie bezposrednie jest niemozliwe. # # Potrzebny uczestnikom za restrykcyjnym NAT-em albo firewallem firmowym, # czyli w praktyce **wiekszosci uczestnikow szkolen korporacyjnych**. # Bez TURN czesc osob dolaczy do pokoju i nie zobaczy ani nie uslyszy nikogo. # Redis: kanal komunikacji miedzy serwerem LiveKit a Egress. # # Egress nie rozmawia z serwerem bezposrednio, tylko przez Redisa: serwer # wrzuca tam zadanie nagrania, Egress je odbiera. **Bez Redisa serwer # przyjmuje zadanie i nie ma go komu przekazac**, a nagrywanie konczy sie # timeoutem przy poprawnie dzialajacym serwerze. # # W tym samym profilu co reszta wideo: instancja bez nagrywania nie ma # powodu trzymac Redisa. redis: image: redis:7-alpine container_name: webinar-redis profiles: ["livekit"] restart: unless-stopped command: redis-server --save "" --appendonly no ports: # Wystawiony **wylacznie na petli zwrotnej** i tylko po to, zeby Egress # w sieci hosta (uruchomienie lokalne) mial jak sie polaczyc. Na produkcji, # gdzie Egress zostaje w sieci Dockera, te linie mozna usunac. - "127.0.0.1:${REDIS_PORT_EXPOSED:-6379}:6379" # Bez wolumenu i bez zapisu na dysk: to kolejka zadan na chwile, a nie # magazyn. Utrata jej zawartosci przy restarcie nic nie kosztuje. # Egress: nagrywa pokoj i zapisuje plik prosto do MinIO. # # DLACZEGO OSOBNY KONTENER # ------------------------------------------------------------------------- # Egress uruchamia **prawdziwa przegladarke** (Chrome), wchodzi nia do pokoju # i nagrywa to, co widzi. Stad wymagania nietypowe dla zwyklej uslugi: # uprawnienie SYS_ADMIN dla piaskownicy Chrome i powiekszony /dev/shm, bo # domyslne 64 MB nie wystarcza przegladarce i konczy sie jej wysypaniem # w polowie nagrania. livekit-egress: # Przypiete do tej samej wersji co w produkcji. `latest` pobieralo # nieprzewidywalny obraz, a niezgodnosc z serwerem LiveKit ujawnia sie # dopiero przy probie nagrania. image: livekit/egress:v1.9 container_name: webinar-livekit-egress profiles: ["livekit"] restart: unless-stopped depends_on: - livekit - redis environment: # Konfiguracja idzie zmienna, a nie plikiem: Egress czyta ja w calosci # z EGRESS_CONFIG_BODY, wiec nie ma czego montowac. # # `ws_url` i `redis` musza wskazywac adresy **w sieci Dockera**, a nie # te widziane z przegladarki: to ruch miedzy uslugami. EGRESS_CONFIG_BODY: | redis: address: ${EGRESS_REDIS_ADDR:-redis:6379} api_key: ${LIVEKIT_API_KEY} api_secret: ${LIVEKIT_API_SECRET} ws_url: ${EGRESS_WS_URL:-ws://livekit:7880} insecure: true log_level: info template_base: ${EGRESS_TEMPLATE_BASE:-http://frontend/} cap_add: # Piaskownica Chrome. Bez tego przegladarka nie wstaje i nagranie # konczy sie bledem jeszcze przed wejsciem do pokoju. - SYS_ADMIN # Pamiec wspoldzielona przegladarki. Domyslne 64 MB wystarcza na kilka # sekund nagrania, potem Chrome sie wysypuje. shm_size: 1gb coturn: image: coturn/coturn:4.6 container_name: webinar-coturn profiles: ["livekit"] restart: unless-stopped network_mode: host command: > -n --realm=${TURN_REALM:-webinar.local} --user=${TURN_USER:-webinar}:${TURN_PASSWORD} --lt-cred-mech --fingerprint --no-cli --no-tlsv1 --no-tlsv1_1 --min-port=${TURN_MIN_PORT:-49160} --max-port=${TURN_MAX_PORT:-49200} # network_mode: host, bo TURN musi widziec prawdziwe adresy IP: # za NAT-em Dockera rozdawalby uczestnikom adresy wewnetrzne kontenera, # pod ktore nikt z zewnatrz sie nie polaczy volumes: postgres-data: minio-data: