Files
akademiawodociagowca/docker-compose.prod.yml
T
debian c53ad95104
Deploy PROD / deploy (push) Failing after 0s
LiveKit: jeden port UDP dla wszystkich (mux) zamiast zakresu
Zakres port_range 50000-50100 rezerwowal osobny port na kazde polaczenie.
Test z 200 botami wszedl dokladnie w 49, a log serwera pokazal kandydata
UDP tylko przy 52 polaczeniach i TCP przy wszystkich 204: kolejni
uczestnicy dostawali juz tylko TCP. Zmienna UDP_PORT z zakresem nie
wlaczala muxa, bo przegrywala z zakresem z pliku.

Teraz udp_port: 7882 (sprawdzone lokalnie: rtc.portUDP Start 7882, End 0),
nowa zmienna LIVEKIT_UDP_PORT, zeby stara LIVEKIT_UDP_RANGE w .env nie
mogla przywrocic zakresu.

Automatyczne wdrozenie nie dotyka profilu livekit: kontener trzeba
przeladowac recznie.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 09:16:01 +02:00

303 lines
12 KiB
YAML

# 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_PORT:-7882}
# 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. Jeden port dla
# wszystkich (tryb mux, opis w livekit.yaml). Bez otwarcia go na
# firewallu serwera uczestnicy widza liste obecnych, ale sie nie slysza.
- "${LIVEKIT_UDP_PORT:-7882}:${LIVEKIT_UDP_PORT:-7882}/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: