# Konfiguracja produkcyjna instancji klienta. # # Roznice wobec docker-compose.yml: # - baza i MinIO nie maja wystawionych portow na zewnatrz, sa dostepne wylacznie # w sieci Dockera. Dostep do bazy z zewnatrz przez tunel SSH, nie przez otwarty port. # - frontend nasluchuje na 127.0.0.1, wiec ruch z internetu musi przejsc przez # reverse proxy klienta, ktore konczy SSL. # - restart: unless-stopped na kazdej usludze. # # - obrazy uslug zewnetrznych sa przypiete do wersji, zeby ponowne wdrozenie # dalo ten sam stan co poprzednie. # # Uruchomienie: # docker compose -f docker-compose.prod.yml --env-file .env.production up -d # # Z wlasnym serwerem wideo i nagrywaniem (cztery dodatkowe uslugi): # docker compose -f docker-compose.prod.yml --env-file .env.production \ # --profile livekit up -d # # PRZED PIERWSZYM URUCHOMIENIEM: ./scripts/check-config.sh services: postgres: image: postgres:16 restart: unless-stopped environment: POSTGRES_DB: ${DB_NAME} POSTGRES_USER: ${DB_USER} POSTGRES_PASSWORD: ${DB_PASSWORD} volumes: - postgres-data:/var/lib/postgresql/data # brak sekcji ports: baza nie jest wystawiona poza siec Dockera healthcheck: test: ["CMD-SHELL", "pg_isready -U ${DB_USER} -d ${DB_NAME}"] interval: 10s timeout: 5s retries: 10 logging: driver: json-file options: max-size: "10m" max-file: "3" minio: image: minio/minio:RELEASE.2024-12-18T13-15-44Z 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 # brak sekcji ports: konsola MinIO takze nie jest wystawiona. # Dostep do konsoli przez tunel: ssh -L 9001:localhost:9001 user@serwer # a nastepnie docker compose port minio 9001, albo tymczasowe przekierowanie. healthcheck: test: ["CMD", "mc", "ready", "local"] interval: 10s timeout: 5s retries: 10 logging: driver: json-file options: max-size: "10m" max-file: "3" backend: build: context: ./backend image: webinar-backend:${APP_VERSION:-latest} 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:-120} ADMIN_EMAIL: ${ADMIN_EMAIL} ADMIN_PASSWORD: ${ADMIN_PASSWORD} SWAGGER_ENABLED: ${SWAGGER_ENABLED:-false} MINIO_ENDPOINT: http://minio:9000 MINIO_ACCESS_KEY: ${MINIO_ACCESS_KEY} MINIO_SECRET_KEY: ${MINIO_SECRET_KEY} MINIO_BUCKET: ${MINIO_BUCKET} # Konfiguracji SMTP i bramki SMS nie przekazujemy przez srodowisko. # Ustawia sie ja w panelu administracyjnym, w sekcji Ustawienia, a wartosci # trafiaja do tabeli app_settings. Zmienne srodowiskowe mialyby nizszy # priorytet, ale ich obecnosc tutaj myliloby co jest zrodlem prawdy. # # WIDEO IDZIE INACZEJ, I TO JEST SWIADOME # ------------------------------------------------------------------- # Dostawca wideo i jego klucze sa TUTAJ, a nie w panelu, bo naleza do # infrastruktury instancji, tak samo jak adres MinIO czy baza: stawia # sie je razem z serwerem, a nie zmienia z tygodnia na tydzien. Adres # LiveKit musi przy tym zgadzac sie z domena, ktora obsluguje nginx # hosta, wiec i tak ustawia go ten, kto wdraza, a nie administrator # tresci. # # BEZ TYCH CZTERECH LINII WIDEO NIE DZIALA WCALE. `SettingsService` # czyta baze, potem srodowisko, potem wartosc domyslna, wiec puste # `app_settings` jest tu stanem NORMALNYM, a nie brakiem konfiguracji: # zapytanie do bazy o klucze LIVEKIT nie zwroci niczego takze na # dzialajacej instancji. Stan faktyczny czyta sie z wypisu # `SettingsStartupReport` w logu backendu. VIDEO_PROVIDER: ${VIDEO_PROVIDER:-none} # Adres widziany przez PRZEGLADARKE: publiczna domena z waznym # certyfikatem, bo laczy sie z nia klient po wss://. LIVEKIT_URL: ${LIVEKIT_URL:-} # Adres widziany przez BACKEND, do wywolan API i Egress. Puste oznacza # ten sam co publiczny. W sieci Dockera to `ws://livekit:7880`: backend # nie musi wychodzic na zewnatrz i wracac przez nginx po to, zeby # porozmawiac z kontenerem obok. LIVEKIT_INTERNAL_URL: ${LIVEKIT_INTERNAL_URL:-} LIVEKIT_API_KEY: ${LIVEKIT_API_KEY:-} LIVEKIT_API_SECRET: ${LIVEKIT_API_SECRET:-} CORS_ALLOWED_ORIGINS: ${CORS_ALLOWED_ORIGINS} LOG_LEVEL_ROOT: ${LOG_LEVEL_ROOT:-INFO} LOG_LEVEL_APP: ${LOG_LEVEL_APP:-INFO} # brak sekcji ports: backend jest dostepny tylko przez frontend w sieci Dockera # # Healthcheck na wlasnym endpointcie /api/health. Bez niego `docker compose ps` # pokazuje kontener jako dzialajacy takze wtedy, gdy aplikacja wstala, ale # nie polaczyla sie z baza: proces zyje, a zadne zadanie nie przechodzi. # `start_period` jest dlugi, bo pierwszy start wykonuje migracje Flyway. healthcheck: test: ["CMD-SHELL", "wget -qO- http://localhost:8080/api/health || exit 1"] interval: 30s timeout: 5s retries: 3 start_period: 90s logging: driver: json-file options: max-size: "10m" max-file: "5" frontend: build: context: ./frontend image: webinar-frontend:${APP_VERSION:-latest} restart: unless-stopped depends_on: backend: # czekamy na zdrowy backend, nie na sam uruchomiony proces: inaczej # pierwsze wejscie na strone po wdrozeniu konczy sie bledem 502 condition: service_healthy ports: # Nasluch tylko na petli lokalnej. Reverse proxy klienta (nginx, Caddy, # Traefik) konczy SSL i przekazuje ruch na ten port po HTTP. - "127.0.0.1:${FRONTEND_PORT_EXPOSED:-8081}:80" logging: driver: json-file options: max-size: "10m" max-file: "3" # ========================================================================== # PROFIL LIVEKIT: wlasny serwer wideo z nagrywaniem # ========================================================================== # # Cztery uslugi, wylaczone domyslnie. Instancja w trybie NONE albo na Jitsi # ich nie uruchamia i nie musi otwierac zadnych dodatkowych portow. # # docker compose -f docker-compose.prod.yml --env-file .env.production \ # --profile livekit up -d # # SAM PROFIL NIE WYSTARCZY: w panelu trzeba jeszcze ustawic VIDEO_PROVIDER # na LIVEKIT oraz klucze API, inaczej aplikacja nie wie, ze serwer istnieje. livekit: # Wersja serwera musi nadazac za `livekit-client` we froncie. Klient 2.22 # laczy sie sciezka `/rtc/v1`, ktorej serwer 1.8 nie zna: sala wyglada na # dzialajaca, ale nie plynie ani obraz, ani dzwiek. image: livekit/livekit-server:v1.13 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}" UDP_PORT: ${LIVEKIT_UDP_RANGE:-50000-50100} # NA PRODUKCJI: publiczny adres IP serwera. Bez niego uczestnicy wchodza # do pokoju i sie nie slysza, bo serwer rozglasza adres nieosiagalny # z internetu. Ustawienie `use_external_ip: true` w livekit.yaml # NADPISUJE ta wartosc, wiec nie uzywac obu naraz. 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 audio i wideo. Bez otwarcia tego # zakresu na firewallu serwera uczestnicy widza liste obecnych, ale # sie nie slysza. Najczestsza przyczyna zgloszenia "wideo nie dziala". - "${LIVEKIT_UDP_RANGE:-50000-50100}:${LIVEKIT_UDP_RANGE:-50000-50100}/udp" logging: driver: json-file options: max-size: "10m" max-file: "3" # Redis: kanal miedzy serwerem LiveKit a Egress. # # Serwer wrzuca tu zadanie nagrania, Egress je odbiera. BEZ REDISA serwer # przyjmuje zadanie i nie ma go komu przekazac: nagrywanie konczy sie # timeoutem przy poprawnie dzialajacym serwerze i dzialajacych spotkaniach. redis: image: redis:7-alpine profiles: ["livekit"] restart: unless-stopped command: redis-server --save "" --appendonly no # Brak sekcji ports, inaczej niz w konfiguracji lokalnej: na produkcji # Egress zostaje w sieci Dockera i laczy sie po nazwie uslugi, wiec # wystawianie Redisa na host byloby otwarciem kolejki zadan bez potrzeby. # Bez wolumenu: to kolejka na chwile, a nie magazyn. logging: driver: json-file options: max-size: "10m" max-file: "3" # Egress: nagrywa pokoj i zapisuje plik prosto do MinIO. # # Uruchamia PRAWDZIWA przegladarke (Chrome), wchodzi nia do pokoju i nagrywa # to, co widzi. Stad wymagania nietypowe dla zwyklej uslugi: SYS_ADMIN dla # piaskownicy Chrome i powiekszony /dev/shm, bo domyslne 64 MB konczy sie # wysypaniem przegladarki w polowie nagrania. livekit-egress: # Wersja przypieta, nie `latest`: obraz Egress zmienia sie czesto, # a niezgodnosc z serwerem ujawnia sie dopiero przy probie nagrania. image: livekit/egress:v1.9 profiles: ["livekit"] restart: unless-stopped depends_on: - livekit - redis environment: # Egress czyta cala konfiguracje z tej zmiennej, wiec nie ma czego montowac. # `ws_url` i `redis` wskazuja 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 shm_size: 1gb logging: driver: json-file options: max-size: "10m" max-file: "3" # 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. coturn: image: coturn/coturn:4.6 profiles: ["livekit"] restart: unless-stopped # 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. 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} logging: driver: json-file options: max-size: "10m" max-file: "3" volumes: postgres-data: minio-data: