Skrypt wdrozenia w repozytorium, dokumentacja komend compose
Deploy PROD / deploy (push) Successful in 10s

SKRYPT WDROZENIA
`deploy-prod.sh` istnial wylacznie na serwerze, tak samo jak wczesniej cztery
zmienne LiveKit w compose: kto odtwarzalby instancje z repozytorium, nie
dostalby go wcale. Wchodzi do `scripts/` w wersji dzialajacej, z jedna zmiana:
sprawdzeniem czystosci repozytorium na starcie.

Powod jest praktyczny. `git merge --ff-only` odrzuca wdrozenie, gdy na serwerze
sa lokalne zmiany, ale robi to CICHO: deploy konczy sie statusem "sukces",
a kod nie wchodzi na serwer. Zdarzylo sie to dzis i wykrylo dopiero reczne
`git log` na produkcji. Teraz skrypt przerywa i wypisuje pliki.

NAZWA PROJEKTU W KOMENDACH COMPOSE
README uczyl komend produkcyjnych BEZ `-p`, czyli dokladnie tych, ktore
wchodza w cudzy projekt. Katalog aplikacji nazywa sie `app`, wiec compose
bierze z niego nazwe projektu, a na serwerze z kilkoma aplikacjami trafia
w nie te. Zdarzylo sie realnie: `up -d --force-recreate coturn` utworzylo
kontener w projekcie drugiej aplikacji tego samego wlasciciela, walczacy
o ten sam port 3478. Jedynym ostrzezeniem byl komunikat "Found orphan
containers", latwy do przeoczenia.

Poprawione 11 komend produkcyjnych plus dwie nowe sekcje: o nazwie projektu
i o tym, ze `docker restart` NIE przeladowuje zmiennych z `.env` (argumenty
sa zamrozone przy tworzeniu kontenera, wiec po zmianie hasla kontener wstaje
ze stara wartoscia, wygladajac na dzialajacy poprawnie). Komendy lokalne
zostaja bez `-p`: tam nazwa z katalogu nikomu nie wchodzi w droge.

INSTRUKCJA TWORZENIA WEBINARU
Uzupelniona o funkcje dodane w ostatnich dniach: link do sali trafiajacy do
uczestnika automatycznie (potwierdzenie, przypomnienia, strona zgloszenia),
nagrywanie wlaczane przyciskiem w panelu prowadzenia oraz rozmycie tla.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-04 11:27:30 +02:00
parent d567ac861b
commit 2c3fbc6236
3 changed files with 141 additions and 12 deletions
+55 -11
View File
@@ -373,7 +373,7 @@ rozjedzie sie z kodem przy dodaniu nowego ustawienia.
### Krok 4: uruchomienie ### Krok 4: uruchomienie
```bash ```bash
docker compose -f docker-compose.prod.yml --env-file .env.production up -d --build docker compose -p akademia -f docker-compose.prod.yml --env-file .env.production up -d --build
``` ```
Pierwszy build pobiera zaleznosci Mavena i npm, wiec trwa kilka minut. Pierwszy build pobiera zaleznosci Mavena i npm, wiec trwa kilka minut.
@@ -381,7 +381,7 @@ Pierwszy build pobiera zaleznosci Mavena i npm, wiec trwa kilka minut.
Sprawdz, czy wszystkie kontenery dzialaja: Sprawdz, czy wszystkie kontenery dzialaja:
```bash ```bash
docker compose -f docker-compose.prod.yml --env-file .env.production ps docker compose -p akademia -f docker-compose.prod.yml --env-file .env.production ps
``` ```
### Krok 5: haslo administratora i pierwsze logowanie ### Krok 5: haslo administratora i pierwsze logowanie
@@ -389,7 +389,7 @@ docker compose -f docker-compose.prod.yml --env-file .env.production ps
Jesli `ADMIN_PASSWORD` bylo puste, odczytaj wygenerowane haslo z logu: Jesli `ADMIN_PASSWORD` bylo puste, odczytaj wygenerowane haslo z logu:
```bash ```bash
docker compose -f docker-compose.prod.yml --env-file .env.production \ docker compose -p akademia -f docker-compose.prod.yml --env-file .env.production \
logs backend | grep -A6 "UTWORZONO PIERWSZE KONTO" logs backend | grep -A6 "UTWORZONO PIERWSZE KONTO"
``` ```
@@ -461,24 +461,68 @@ Sprawdz jeszcze:
```bash ```bash
git pull git pull
docker compose -f docker-compose.prod.yml --env-file .env.production up -d --build docker compose -p akademia -f docker-compose.prod.yml --env-file .env.production up -d --build
``` ```
Migracje Flyway wykonuja sie automatycznie przy starcie backendu. Przed Migracje Flyway wykonuja sie automatycznie przy starcie backendu. Przed
aktualizacja wykonaj kopie zapasowa, patrz sekcja nizej. aktualizacja wykonaj kopie zapasowa, patrz sekcja nizej.
### Nazwa projektu jest obowiazkowa, gdy serwer dzieli kilka aplikacji
**`-p <nazwa>` w kazdej komendzie `docker compose`.** Bez tej flagi compose
bierze nazwe projektu **z katalogu roboczego**, a katalogi bywaja nazwane
ogolnie (`app`, `www`, `docker`). Na serwerze z kilkoma aplikacjami konczy sie
to wejsciem w cudzy projekt: `up -d` restartuje wtedy nie te uslugi, a
`up -d <usluga>` tworzy obcy kontener walczacy o ten sam port hosta.
Zdarzylo sie to realnie na instancji, gdzie katalog nazywal sie `app`, a na tym
samym serwerze stala druga aplikacja w projekcie o tej samej nazwie. Jedynym
ostrzezeniem byl latwy do przeoczenia komunikat "Found orphan containers".
Nazwa projektu ma odpowiadac instancji, nie katalogowi. Po kazdej recznej
komendzie warto sprawdzic, czy nie przybylo kontenerow z obcym prefiksem:
```bash
docker ps --format '{{.Label "com.docker.compose.project"}}\t{{.Names}}\t{{.Status}}' | sort
```
### Zmienne z `.env` wymagaja odtworzenia kontenera, nie restartu
**`docker restart` NIE przeladowuje `.env`.** Argumenty uruchomienia sa
zamrozone w konfiguracji kontenera przy jego tworzeniu, wiec po zmianie hasla
albo klucza kontener wstaje ze **stara wartoscia**, wygladajac przy tym na
dzialajacy poprawnie. Objawia sie to dopiero tym, ze usluga cicho nie
uwierzytelnia sie u drugiej strony.
```bash
# zmiana wartosci w .env: konieczne odtworzenie
docker compose -p akademia -f docker-compose.prod.yml --env-file .env.production up -d --force-recreate coturn
# sprawdzenie, ze kontener ma wartosc z pliku, bez pokazywania sekretu
docker inspect akademia-coturn-1 --format '{{json .Config.Cmd}}' | grep -o 'webinar:[^"]*' | cut -d: -f2 | sha256sum | cut -c1-16
grep '^TURN_PASSWORD=' .env.production | cut -d= -f2- | sha256sum | cut -c1-16
```
### Zatrzymanie i restart ### Zatrzymanie i restart
```bash ```bash
# restart pojedynczej uslugi # restart pojedynczej uslugi (bez zmian w .env)
docker compose -f docker-compose.prod.yml --env-file .env.production restart backend docker compose -p akademia -f docker-compose.prod.yml --env-file .env.production restart backend
# zatrzymanie calosci, dane w wolumenach zostaja # zatrzymanie calosci, dane w wolumenach zostaja
docker compose -f docker-compose.prod.yml --env-file .env.production down docker compose -p akademia -f docker-compose.prod.yml --env-file .env.production down
``` ```
Nie uzywaj `down -v` na produkcji: ta komenda usuwa wolumeny wraz z danymi. Nie uzywaj `down -v` na produkcji: ta komenda usuwa wolumeny wraz z danymi.
### Wdrozenie automatyczne
`scripts/deploy-prod.sh` pobiera zmiany z `main` i przebudowuje obrazy.
Wolany przez Gitea Actions po kazdym pushu, dziala takze recznie. Skrypt
**przerywa wdrozenie, gdy repozytorium na serwerze ma lokalne zmiany**:
`git merge --ff-only` i tak by ich nie nadpisal, ale robil to cicho, wiec
deploy konczyl sie statusem "sukces", a kod nie wchodzil na serwer.
## Kopia zapasowa i odtworzenie ## Kopia zapasowa i odtworzenie
### Wykonanie kopii ### Wykonanie kopii
@@ -554,7 +598,7 @@ wymaga **okolo 170 GB**. Przy takiej skali warto:
docker system df -v | grep minio-data docker system df -v | grep minio-data
# baza # baza
docker compose -f docker-compose.prod.yml exec -T postgres \ docker compose -p akademia -f docker-compose.prod.yml exec -T postgres \
psql -U webinar -d webinar -c "SELECT pg_size_pretty(pg_database_size('webinar'));" psql -U webinar -d webinar -c "SELECT pg_size_pretty(pg_database_size('webinar'));"
``` ```
@@ -602,7 +646,7 @@ oraz zaloguj sie do panelu i obejrzyj liste webinarow i zapisow.
2. Skopiuj `env.production.kopia` z kopii jako `.env.production` i ustaw 2. Skopiuj `env.production.kopia` z kopii jako `.env.production` i ustaw
`chmod 600`. Zachowaj **ten sam** `JWT_SECRET`, inaczej wszyscy zostana `chmod 600`. Zachowaj **ten sam** `JWT_SECRET`, inaczej wszyscy zostana
wylogowani, oraz to samo `DB_PASSWORD`, bo odtwarzana baza go uzywa. wylogowani, oraz to samo `DB_PASSWORD`, bo odtwarzana baza go uzywa.
3. Uruchom uslugi: `docker compose -f docker-compose.prod.yml --env-file .env.production up -d`. 3. Uruchom uslugi: `docker compose -p akademia -f docker-compose.prod.yml --env-file .env.production up -d`.
4. Odtworz dane: `./scripts/restore.sh <katalog-kopii>`. 4. Odtworz dane: `./scripts/restore.sh <katalog-kopii>`.
5. Przekieruj domene na nowy serwer i sprawdz `/api/health`. 5. Przekieruj domene na nowy serwer i sprawdz `/api/health`.
@@ -634,7 +678,7 @@ albo na Jitsi nie uruchamia niczego zbednego.
```bash ```bash
# uzupelnij w .env: LIVEKIT_API_KEY, LIVEKIT_API_SECRET, TURN_PASSWORD # uzupelnij w .env: LIVEKIT_API_KEY, LIVEKIT_API_SECRET, TURN_PASSWORD
docker compose --profile livekit up -d docker compose -p akademia --profile livekit -f docker-compose.prod.yml --env-file .env.production up -d
``` ```
Ten sam klucz i sekret wpisz w panelu (`Ustawienia > Wideo`): tutaj konfiguruja Ten sam klucz i sekret wpisz w panelu (`Ustawienia > Wideo`): tutaj konfiguruja
@@ -859,7 +903,7 @@ kart testowych zmieniaja sie miedzy wersjami uslugi.
- **Komunikat odrzucony tez jest zapisany**, z oznaczeniem `podpis niepoprawny`. - **Komunikat odrzucony tez jest zapisany**, z oznaczeniem `podpis niepoprawny`.
Brak wpisu oznacza, ze notyfikacja w ogole nie dotarla: sprawdz adres Brak wpisu oznacza, ze notyfikacja w ogole nie dotarla: sprawdz adres
w panelu operatora i czy domena ma wazny certyfikat. w panelu operatora i czy domena ma wazny certyfikat.
- `docker compose logs -f backend | grep -i autopay` - `docker compose -p akademia -f docker-compose.prod.yml logs -f backend | grep -i autopay`
### Reczne nadanie dostepu ### Reczne nadanie dostepu
+21 -1
View File
@@ -319,6 +319,7 @@
<span class="chip">Rola: ADMIN lub MODERATOR</span> <span class="chip">Rola: ADMIN lub MODERATOR</span>
<span class="chip">8 zakładek</span> <span class="chip">8 zakładek</span>
<span class="chip">Zapis jako szkic</span> <span class="chip">Zapis jako szkic</span>
<span class="chip">Stan na 4 września 2026</span>
</div> </div>
</header> </header>
@@ -485,6 +486,12 @@
<span class="name">Po webinarze</span> <span class="tech">adres nagrania, adres materiałów</span> <span class="name">Po webinarze</span> <span class="tech">adres nagrania, adres materiałów</span>
<p>Oba adresy trafiają do <strong>maila podsumowującego</strong> wysyłanego automatycznie po zakończeniu. Uzupełnij je przed wysyłką podsumowania; jeśli nagranie pojawi się później, jest osobny przycisk do wysłania powiadomienia o nagraniu.</p> <p>Oba adresy trafiają do <strong>maila podsumowującego</strong> wysyłanego automatycznie po zakończeniu. Uzupełnij je przed wysyłką podsumowania; jeśli nagranie pojawi się później, jest osobny przycisk do wysłania powiadomienia o nagraniu.</p>
</div> </div>
<div class="note tip">
<span class="ic">💡</span>
<p><strong>Linku do spotkania nie musisz nigdzie wpisywać ani wysyłać.</strong> Uczestnik dostaje go automatycznie: w mailu potwierdzającym zapis, w każdym przypomnieniu oraz na stronie swojego zgłoszenia. Sala otwiera się krótko przed startem &mdash; kto kliknie wcześniej, zobaczy odliczanie zamiast błędu.</p>
<p style="margin-top:8px">Dotyczy to dostawców z własnym pokojem (LiveKit, Jitsi, Daily, Zoom). W trybie <span class="prov">NONE</span> obowiązuje link, który wkleisz w polu „Link do spotkania”.</p>
</div>
<div class="field"> <div class="field">
<span class="name">Nagrania i materiały</span> <span class="name">Nagrania i materiały</span>
<p>Osobna zakładka na wgranie plików do własnego składu (po zapisaniu webinaru). Alternatywa dla wklejania adresów zewnętrznych.</p> <p>Osobna zakładka na wgranie plików do własnego składu (po zapisaniu webinaru). Alternatywa dla wklejania adresów zewnętrznych.</p>
@@ -514,7 +521,7 @@
<tr><th>Dostawca</th><th>Uprawnienia</th><th>Nagrywanie</th></tr> <tr><th>Dostawca</th><th>Uprawnienia</th><th>Nagrywanie</th></tr>
</thead> </thead>
<tbody> <tbody>
<tr><td><span class="prov">LIVEKIT</span><br><span style="color:var(--ink-faint);font-size:.82rem">domyślny</span></td><td>Egzekwuje serwer (token). Uczestnik nie włączy kamery, nawet podmieniając stronę.</td><td>Automatyczne (wymaga usług Egress + Redis).</td></tr> <tr><td><span class="prov">LIVEKIT</span><br><span style="color:var(--ink-faint);font-size:.82rem">domyślny</span></td><td>Egzekwuje serwer (token). Uczestnik nie włączy kamery, nawet podmieniając stronę.</td><td>Prowadzący włącza przyciskiem w panelu prowadzenia. Plik trafia na Wasz serwer (wymaga usług Egress + Redis).</td></tr>
<tr><td><span class="prov">DAILY</span></td><td>Egzekwuje serwer.</td><td>Pobierane od dostawcy po zakończeniu.</td></tr> <tr><td><span class="prov">DAILY</span></td><td>Egzekwuje serwer.</td><td>Pobierane od dostawcy po zakończeniu.</td></tr>
<tr><td><span class="prov">ZOOM</span></td><td>Osadzone w oknie. Role jak przy Jitsi.</td><td>Po stronie Zoom.</td></tr> <tr><td><span class="prov">ZOOM</span></td><td>Osadzone w oknie. Role jak przy Jitsi.</td><td>Po stronie Zoom.</td></tr>
<tr><td><span class="prov">JITSI</span></td><td>Z JWT: tylko rola moderatora. Bez JWT: pokój otwarty dla każdego, kto zna adres.</td><td>Ręczne wgranie pliku.</td></tr> <tr><td><span class="prov">JITSI</span></td><td>Z JWT: tylko rola moderatora. Bez JWT: pokój otwarty dla każdego, kto zna adres.</td><td>Ręczne wgranie pliku.</td></tr>
@@ -526,6 +533,19 @@
<span class="ic">💡</span> <span class="ic">💡</span>
<p>Wideo uczestników jest <strong>zawsze wyłączone</strong> na poziomie uprawnień. Prowadzący może w trakcie webinaru włączyć głos wybranej osobie lub — świadomie — kamerę dla wszystkich.</p> <p>Wideo uczestników jest <strong>zawsze wyłączone</strong> na poziomie uprawnień. Prowadzący może w trakcie webinaru włączyć głos wybranej osobie lub — świadomie — kamerę dla wszystkich.</p>
</div> </div>
<p style="margin-top:14px"><strong>Co prowadzący ma pod ręką w trakcie spotkania</strong> (Panel &rarr; Webinary &rarr; „Prowadzenie”):</p>
<ul class="plain">
<li><strong>Otwarty mikrofon</strong> — każdy mówi bez proszenia o głos. Włączenie pyta o potwierdzenie, bo otwiera mikrofony całej sali naraz.</li>
<li><strong>Wideo dla wszystkich</strong> — każdy uczestnik może włączyć kamerę.</li>
<li><strong>Nagraj spotkanie</strong> — start i stop nagrywania. Nagranie <strong>nie startuje samo</strong>: to świadoma decyzja organizatora, a zgoda uczestników na nagranie pozostaje jego sprawą. Gotowy plik pojawia się w zakładce „Nagrania i materiały” w ciągu minuty od zatrzymania.</li>
<li><strong>Kolejka zgłoszeń do głosu</strong>, moderacja pytań i ogłaszanie zadań.</li>
</ul>
<div class="note tip">
<span class="ic">💡</span>
<p><strong>Rozmycie tła.</strong> Każdy, kto ma włączoną kamerę — prowadzący i prelegenci — może rozmyć swoje tło przyciskiem w lewym górnym rogu okna wideo. Włącza się i wyłącza w dowolnym momencie, bez przerywania obrazu. Przydaje się przy prowadzeniu z domu.</p>
</div>
</div> </div>
</div> </div>
</div> </div>
+65
View File
@@ -0,0 +1,65 @@
#!/usr/bin/env bash
#
# Wdrozenie na serwer produkcyjny.
#
# Wolane przez Gitea Actions (`.gitea/workflows/deploy.yml`) po kazdym pushu
# na `main`, ale dziala takze recznie. Kopia na serwerze lezy w
# `/opt/akademia/deploy-prod.sh` i musi byc zgodna z ta wersja.
#
# NAZWA PROJEKTU JEST OBOWIAZKOWA
# -----------------------------------------------------------------------------
# `-p akademia` NIE jest ozdobnikiem. Katalog aplikacji nazywa sie
# `/opt/akademia/app`, wiec bez tej flagi compose bierze nazwe projektu
# z katalogu i staje sie projektem `app`. Na tym serwerze `app` to **inna
# aplikacja tego samego wlasciciela**, a komenda bez `-p` tworzy w niej obce
# kontenery albo restartuje cudze uslugi. Zdarzylo sie to realnie.
#
set -euo pipefail
APP_DIR=/opt/akademia/app
LOG=/opt/akademia/deploy.log
COMPOSE="docker compose -p akademia -f docker-compose.prod.yml --env-file .env.production"
# Dwa deploye naraz przebudowywalyby ten sam obraz i restartowaly kontener
# w polowie pracy drugiego. Blokada jest nieblokujaca: drugi przebieg konczy
# sie od razu, zamiast czekac w kolejce z nieaktualnym juz kodem.
exec 9>/opt/akademia/deploy.lock
flock -n 9 || { echo "$(date '+%F %T') inny deploy trwa - pomijam"; exit 0; }
exec >>"$LOG" 2>&1
echo "===== $(date '+%F %T') deploy start ====="
cd "$APP_DIR"
# Lokalne zmiany zatrzymuja wdrozenie, i to jest swiadome: `--ff-only` nizej
# i tak by je odrzucil, ale robil to CICHO. Deploy konczyl sie wtedy statusem
# "sukces", a kod nie wchodzil na serwer: rozjazd wykrywalo sie dopiero
# recznym `git log` na produkcji. Lepiej zaswiecic na czerwono i nazwac pliki.
if [ -n "$(git status --porcelain)" ]; then
echo "PRZERWANE: repozytorium na serwerze ma lokalne zmiany."
echo "Wdrozenie nie ruszy, dopoki ktos ich nie przeniesie albo nie odrzuci:"
git status --short
echo
echo "Zmiana warta zachowania: przenies ja do repozytorium."
echo "Zmiana do wyrzucenia: git checkout -- <plik>"
exit 1
fi
git fetch --prune origin
LOCAL=$(git rev-parse @)
REMOTE=$(git rev-parse '@{u}')
if [ "$LOCAL" = "$REMOTE" ]; then
echo "Brak zmian ($LOCAL) - koniec."
exit 0
fi
echo "Aktualizacja $LOCAL -> $REMOTE"
# --ff-only: nigdy nie scalamy na produkcji. Merge z konfliktem zostawilby
# repozytorium w stanie polowicznym, a rozwiazywanie go przez SSH na zywej
# instancji jest ostatnia rzecza, ktora ktokolwiek chce robic.
git merge --ff-only origin/main
$COMPOSE up -d --build
echo "===== $(date '+%F %T') deploy OK (v $REMOTE) ====="