Files
akademiawodociagowca/docker-compose.prod.yml
T
Debian b8f91d94b5
Deploy PROD / deploy (push) Successful in 0s
Configure isolated production deployment
2026-09-02 09:19:26 +02:00

277 lines
10 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.
VIDEO_PROVIDER: ${VIDEO_PROVIDER:-none}
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: