Initial commit
This commit is contained in:
@@ -0,0 +1,164 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Kopia zapasowa instancji: baza PostgreSQL i pliki MinIO.
|
||||
#
|
||||
# Uruchomienie z katalogu projektu:
|
||||
# ./scripts/backup.sh
|
||||
# ./scripts/backup.sh /mnt/backup/webinar
|
||||
#
|
||||
# Na serwerze deweloperskim z Windows i Git Bash trzeba wylaczyc konwersje
|
||||
# sciezek, inaczej tar dostanie sciezke windowsowa:
|
||||
# MSYS_NO_PATHCONV=1 COMPOSE_FILE=docker-compose.yml ENV_FILE=.env ./scripts/backup.sh
|
||||
#
|
||||
# Domyslnie zapisuje do ./backups. Kazda kopia trafia do osobnego katalogu
|
||||
# nazwanego znacznikiem czasu, zeby przypadkiem nie nadpisac poprzedniej.
|
||||
#
|
||||
# Odtworzenie: patrz scripts/restore.sh oraz sekcja w README.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
COMPOSE_FILE="${COMPOSE_FILE:-docker-compose.prod.yml}"
|
||||
ENV_FILE="${ENV_FILE:-.env.production}"
|
||||
BACKUP_ROOT="${1:-./backups}"
|
||||
|
||||
# ile kopii zachowac, starsze sa usuwane
|
||||
KEEP_LAST="${KEEP_LAST:-14}"
|
||||
|
||||
# Kopia bez plikow MinIO: SKIP_MINIO=1
|
||||
#
|
||||
# Nagrania stanowia prawie caly rozmiar kopii (400 MB do 1 GB na godzine),
|
||||
# a pliki w MinIO sa NIEZMIENNE PO WGRANIU: kopia sprzed tygodnia zawiera
|
||||
# dokladnie te same nagrania co dzisiejsza. Przy ograniczonym miejscu warto
|
||||
# robic codziennie sama baze, a calosc raz w tygodniu.
|
||||
#
|
||||
# Zrzut samej bazy wystarcza do odtworzenia konfiguracji, zapisow, kursow
|
||||
# i wynikow. Zginie wylacznie TRESC plikow: wpisy w bazie zostana i beda
|
||||
# wskazywac na nieistniejace obiekty.
|
||||
SKIP_MINIO="${SKIP_MINIO:-0}"
|
||||
|
||||
TIMESTAMP="$(date +%Y%m%d-%H%M%S)"
|
||||
TARGET="${BACKUP_ROOT}/${TIMESTAMP}"
|
||||
|
||||
compose() {
|
||||
docker compose -f "${COMPOSE_FILE}" --env-file "${ENV_FILE}" "$@"
|
||||
}
|
||||
|
||||
log() {
|
||||
echo "[$(date +%H:%M:%S)] $*"
|
||||
}
|
||||
|
||||
if [ ! -f "${ENV_FILE}" ]; then
|
||||
echo "BLAD: brak pliku ${ENV_FILE}. Uruchom skrypt z katalogu projektu." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# nazwa bazy i uzytkownika z konfiguracji instancji
|
||||
DB_NAME="$(grep -E '^DB_NAME=' "${ENV_FILE}" | cut -d= -f2-)"
|
||||
DB_USER="$(grep -E '^DB_USER=' "${ENV_FILE}" | cut -d= -f2-)"
|
||||
|
||||
if [ -z "${DB_NAME}" ] || [ -z "${DB_USER}" ]; then
|
||||
echo "BLAD: nie udalo sie odczytac DB_NAME lub DB_USER z ${ENV_FILE}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "${TARGET}"
|
||||
log "Kopia zapasowa do katalogu ${TARGET}"
|
||||
|
||||
# --- baza danych ---
|
||||
# pg_dump w formacie custom, mniejszy i pozwala na wybiorcze odtwarzanie
|
||||
log "Zrzut bazy ${DB_NAME}..."
|
||||
compose exec -T postgres pg_dump \
|
||||
-U "${DB_USER}" \
|
||||
-d "${DB_NAME}" \
|
||||
--format=custom \
|
||||
--no-owner \
|
||||
> "${TARGET}/baza.dump"
|
||||
|
||||
DB_SIZE="$(du -h "${TARGET}/baza.dump" | cut -f1)"
|
||||
log "Baza zapisana (${DB_SIZE})"
|
||||
|
||||
# --- pliki MinIO ---
|
||||
# Obraz MinIO nie zawiera tar, wiec archiwizujemy wolumen z pomocniczego
|
||||
# kontenera alpine podpietego do tego samego wolumenu w trybie tylko do odczytu.
|
||||
if [ "${SKIP_MINIO}" = "1" ]; then
|
||||
log "Pomijam pliki MinIO (SKIP_MINIO=1)"
|
||||
MINIO_SIZE="pominieto"
|
||||
else
|
||||
log "Kopia plikow MinIO..."
|
||||
|
||||
# Nazwe wolumenu bierzemy prosto z dzialajacego kontenera, zeby nie zgadywac
|
||||
# przedrostka projektu ani nie parsowac konfiguracji compose.
|
||||
MINIO_CONTAINER="$(compose ps -q minio)"
|
||||
if [ -z "${MINIO_CONTAINER}" ]; then
|
||||
echo "BLAD: kontener minio nie dziala, nie mozna wykonac kopii plikow" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
MINIO_VOLUME="$(docker inspect "${MINIO_CONTAINER}" \
|
||||
--format '{{range .Mounts}}{{if eq .Destination "/data"}}{{.Name}}{{end}}{{end}}')"
|
||||
|
||||
if [ -z "${MINIO_VOLUME}" ]; then
|
||||
echo "BLAD: nie udalo sie ustalic wolumenu MinIO" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log " wolumen: ${MINIO_VOLUME}"
|
||||
|
||||
docker run --rm \
|
||||
-v "${MINIO_VOLUME}:/data:ro" \
|
||||
alpine:3.20 tar czf - -C /data . > "${TARGET}/minio.tar.gz"
|
||||
|
||||
# pusty albo podejrzanie maly plik oznacza, ze archiwizacja sie nie udala
|
||||
if [ ! -s "${TARGET}/minio.tar.gz" ] || [ "$(stat -c%s "${TARGET}/minio.tar.gz")" -lt 100 ]; then
|
||||
echo "BLAD: archiwum MinIO jest puste, kopia jest niekompletna" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
MINIO_SIZE="$(du -h "${TARGET}/minio.tar.gz" | cut -f1)"
|
||||
log "Pliki MinIO zapisane (${MINIO_SIZE})"
|
||||
fi
|
||||
|
||||
# --- konfiguracja instancji ---
|
||||
# .env.production zawiera sekrety, wiec kopia musi byc chroniona uprawnieniami
|
||||
cp "${ENV_FILE}" "${TARGET}/env.production.kopia"
|
||||
chmod 600 "${TARGET}/env.production.kopia"
|
||||
log "Konfiguracja zapisana"
|
||||
|
||||
# --- metryczka, zeby przy odtwarzaniu bylo wiadomo co to jest ---
|
||||
cat > "${TARGET}/INFO.txt" <<EOF
|
||||
Kopia zapasowa platformy webinarowej
|
||||
Data: $(date '+%Y-%m-%d %H:%M:%S %Z')
|
||||
Serwer: $(hostname)
|
||||
Baza: ${DB_NAME}
|
||||
Wersja obrazu: $(compose images backend --format json 2>/dev/null | head -c 200 || echo 'nieznana')
|
||||
|
||||
Rodzaj kopii: $([ "${SKIP_MINIO}" = "1" ] && echo 'TYLKO BAZA (bez plikow MinIO)' || echo 'pelna')
|
||||
Rozmiar MinIO: ${MINIO_SIZE}
|
||||
|
||||
Zawartosc:
|
||||
baza.dump zrzut PostgreSQL w formacie custom (pg_restore)
|
||||
minio.tar.gz zawartosc wolumenu MinIO (brak przy SKIP_MINIO=1)
|
||||
env.production.kopia konfiguracja instancji, ZAWIERA SEKRETY
|
||||
|
||||
Odtworzenie: ./scripts/restore.sh ${TARGET}
|
||||
EOF
|
||||
|
||||
chmod 700 "${TARGET}"
|
||||
|
||||
# --- rotacja starych kopii ---
|
||||
if [ "${KEEP_LAST}" -gt 0 ]; then
|
||||
COUNT="$(find "${BACKUP_ROOT}" -maxdepth 1 -mindepth 1 -type d | wc -l)"
|
||||
if [ "${COUNT}" -gt "${KEEP_LAST}" ]; then
|
||||
REMOVE=$((COUNT - KEEP_LAST))
|
||||
log "Usuwam ${REMOVE} najstarszych kopii (zachowuje ${KEEP_LAST})"
|
||||
find "${BACKUP_ROOT}" -maxdepth 1 -mindepth 1 -type d \
|
||||
| sort \
|
||||
| head -n "${REMOVE}" \
|
||||
| while read -r old; do
|
||||
rm -rf "${old}"
|
||||
log " usunieto $(basename "${old}")"
|
||||
done
|
||||
fi
|
||||
fi
|
||||
|
||||
log "Gotowe: ${TARGET}"
|
||||
@@ -0,0 +1,238 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Sprawdzenie konfiguracji przed uruchomieniem instancji u klienta.
|
||||
#
|
||||
# ./scripts/check-config.sh # sprawdza .env.production
|
||||
# ENV_FILE=.env ./scripts/check-config.sh # inny plik
|
||||
# ./scripts/check-config.sh --skip-db # bez pytania bazy (przed pierwszym startem)
|
||||
#
|
||||
# ODMAWIA STARTU, ZAMIAST OSTRZEC I PRZEPUSCIC.
|
||||
# ---------------------------------------------------------------------------
|
||||
# Kazdy sprawdzany warunek to blad, ktory na produkcji kosztuje wiecej niz
|
||||
# nieudany start: sekret z przykladu oznacza, ze kazdy moze podrobic token,
|
||||
# adres z "localhost" oznacza martwe linki w kazdym wyslanym mailu, a haslo
|
||||
# przykladowe oznacza konto administratora otwarte dla kogokolwiek, kto
|
||||
# widzial repozytorium.
|
||||
#
|
||||
# Kod wyjscia: 0 gdy wszystko w porzadku, 1 gdy cokolwiek blokuje start.
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
ENV_FILE="${ENV_FILE:-.env.production}"
|
||||
COMPOSE_FILE="${COMPOSE_FILE:-docker-compose.prod.yml}"
|
||||
EXAMPLE_FILE="${EXAMPLE_FILE:-.env.production.example}"
|
||||
SKIP_DB=0
|
||||
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--skip-db) SKIP_DB=1 ;;
|
||||
-h|--help) sed -n '2,20p' "$0"; exit 0 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
ERRORS=0
|
||||
WARNINGS=0
|
||||
|
||||
red() { printf '\033[31m%s\033[0m\n' "$*"; }
|
||||
yellow() { printf '\033[33m%s\033[0m\n' "$*"; }
|
||||
green() { printf '\033[32m%s\033[0m\n' "$*"; }
|
||||
|
||||
fail() {
|
||||
red " BLAD: $1"
|
||||
[ $# -gt 1 ] && echo " $2"
|
||||
ERRORS=$((ERRORS + 1))
|
||||
}
|
||||
|
||||
warn() {
|
||||
yellow " UWAGA: $1"
|
||||
[ $# -gt 1 ] && echo " $2"
|
||||
WARNINGS=$((WARNINGS + 1))
|
||||
}
|
||||
|
||||
ok() { echo " ok: $1"; }
|
||||
|
||||
# Wartosc zmiennej z pliku .env. Obsluguje wartosci w cudzyslowach.
|
||||
value_of() {
|
||||
local key="$1" file="$2"
|
||||
grep -E "^${key}=" "$file" 2>/dev/null \
|
||||
| tail -n1 \
|
||||
| cut -d= -f2- \
|
||||
| sed -e 's/^"//' -e 's/"$//' -e "s/^'//" -e "s/'$//"
|
||||
}
|
||||
|
||||
echo "=========================================================="
|
||||
echo " Sprawdzenie konfiguracji: ${ENV_FILE}"
|
||||
echo "=========================================================="
|
||||
|
||||
if [ ! -f "${ENV_FILE}" ]; then
|
||||
red "BLAD: brak pliku ${ENV_FILE}"
|
||||
echo
|
||||
echo "Skopiuj wzor i uzupelnij:"
|
||||
echo " cp ${EXAMPLE_FILE} ${ENV_FILE}"
|
||||
echo " chmod 600 ${ENV_FILE}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# --- uprawnienia pliku z sekretami ---
|
||||
echo
|
||||
echo "[1/5] Plik konfiguracji"
|
||||
if [ "$(uname -s)" != "MINGW64_NT"* ] && command -v stat >/dev/null 2>&1; then
|
||||
PERMS="$(stat -c '%a' "${ENV_FILE}" 2>/dev/null || echo '')"
|
||||
case "${PERMS}" in
|
||||
600|400) ok "uprawnienia ${PERMS}" ;;
|
||||
"") ;; # systemy bez stat -c, pomijamy
|
||||
*) warn "plik ma uprawnienia ${PERMS}, zalecane 600" \
|
||||
"chmod 600 ${ENV_FILE}" ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# --- JWT_SECRET ---
|
||||
echo
|
||||
echo "[2/5] Sekret podpisywania tokenow"
|
||||
JWT_SECRET="$(value_of JWT_SECRET "${ENV_FILE}")"
|
||||
JWT_EXAMPLE="$(value_of JWT_SECRET "${EXAMPLE_FILE}" 2>/dev/null || echo '')"
|
||||
|
||||
if [ -z "${JWT_SECRET}" ]; then
|
||||
fail "JWT_SECRET jest pusty" \
|
||||
"Wygeneruj: openssl rand -base64 48"
|
||||
elif [ "${#JWT_SECRET}" -lt 32 ]; then
|
||||
fail "JWT_SECRET ma ${#JWT_SECRET} znakow, wymagane minimum 32" \
|
||||
"Krotszy klucz da sie zlamac. Wygeneruj: openssl rand -base64 48"
|
||||
elif [ -n "${JWT_EXAMPLE}" ] && [ "${JWT_SECRET}" = "${JWT_EXAMPLE}" ]; then
|
||||
fail "JWT_SECRET jest rowny wartosci z ${EXAMPLE_FILE}" \
|
||||
"Kazdy, kto widzial repozytorium, moze podrobic token administratora."
|
||||
else
|
||||
ok "JWT_SECRET ustawiony (${#JWT_SECRET} znakow)"
|
||||
fi
|
||||
|
||||
# --- ADMIN_PASSWORD ---
|
||||
echo
|
||||
echo "[3/5] Konto administratora"
|
||||
ADMIN_PASSWORD="$(value_of ADMIN_PASSWORD "${ENV_FILE}")"
|
||||
ADMIN_EXAMPLE="$(value_of ADMIN_PASSWORD "${EXAMPLE_FILE}" 2>/dev/null || echo '')"
|
||||
ADMIN_EMAIL="$(value_of ADMIN_EMAIL "${ENV_FILE}")"
|
||||
|
||||
if [ -z "${ADMIN_EMAIL}" ]; then
|
||||
fail "ADMIN_EMAIL jest pusty" "Bez adresu nie powstanie pierwsze konto."
|
||||
else
|
||||
ok "ADMIN_EMAIL = ${ADMIN_EMAIL}"
|
||||
fi
|
||||
|
||||
if [ -z "${ADMIN_PASSWORD}" ]; then
|
||||
# Puste haslo jest tu POPRAWNE i zalecane: aplikacja losuje je przy
|
||||
# pierwszym starcie i wypisuje raz w logu. To bezpieczniejsze niz haslo
|
||||
# przechowywane w pliku na serwerze.
|
||||
ok "ADMIN_PASSWORD pusty: haslo zostanie wylosowane i wypisane w logu"
|
||||
echo " Po starcie: docker compose logs backend | grep -i haslo"
|
||||
elif [ -n "${ADMIN_EXAMPLE}" ] && [ "${ADMIN_PASSWORD}" = "${ADMIN_EXAMPLE}" ]; then
|
||||
fail "ADMIN_PASSWORD jest rowny wartosci z ${EXAMPLE_FILE}" \
|
||||
"Konto administratora byloby otwarte dla kazdego, kto widzial repozytorium."
|
||||
elif [ "${#ADMIN_PASSWORD}" -lt 12 ]; then
|
||||
warn "ADMIN_PASSWORD ma ${#ADMIN_PASSWORD} znakow, zalecane minimum 12" \
|
||||
"Albo zostaw puste, a aplikacja wylosuje haslo."
|
||||
else
|
||||
ok "ADMIN_PASSWORD ustawiony"
|
||||
fi
|
||||
|
||||
# --- APP_PUBLIC_URL ---
|
||||
echo
|
||||
echo "[4/5] Adres publiczny instancji"
|
||||
APP_PUBLIC_URL="$(value_of APP_PUBLIC_URL "${ENV_FILE}")"
|
||||
|
||||
if [ -z "${APP_PUBLIC_URL}" ]; then
|
||||
fail "APP_PUBLIC_URL jest pusty" \
|
||||
"Z tego adresu powstaja WSZYSTKIE linki w mailach: potwierdzenia, przypomnienia i wypisanie."
|
||||
elif echo "${APP_PUBLIC_URL}" | grep -qiE 'localhost|127\.0\.0\.1|0\.0\.0\.0'; then
|
||||
fail "APP_PUBLIC_URL zawiera adres lokalny: ${APP_PUBLIC_URL}" \
|
||||
"Uczestnik dostanie link prowadzacy do jego wlasnego komputera."
|
||||
elif ! echo "${APP_PUBLIC_URL}" | grep -qE '^https?://'; then
|
||||
fail "APP_PUBLIC_URL nie zaczyna sie od http:// ani https://: ${APP_PUBLIC_URL}"
|
||||
else
|
||||
case "${APP_PUBLIC_URL}" in
|
||||
*/) warn "APP_PUBLIC_URL konczy sie ukosnikiem" "Usun go: ${APP_PUBLIC_URL%/}" ;;
|
||||
esac
|
||||
echo "${APP_PUBLIC_URL}" | grep -qE '^http://' \
|
||||
&& warn "APP_PUBLIC_URL uzywa http, nie https" \
|
||||
"Tokeny zapisu i sesji poszlyby przez siec otwartym tekstem."
|
||||
ok "APP_PUBLIC_URL = ${APP_PUBLIC_URL}"
|
||||
fi
|
||||
|
||||
# pozostale sekrety infrastruktury
|
||||
for key in DB_PASSWORD MINIO_SECRET_KEY MINIO_ACCESS_KEY DB_NAME DB_USER MINIO_BUCKET; do
|
||||
v="$(value_of "${key}" "${ENV_FILE}")"
|
||||
ex="$(value_of "${key}" "${EXAMPLE_FILE}" 2>/dev/null || echo '')"
|
||||
if [ -z "${v}" ]; then
|
||||
fail "${key} jest pusty"
|
||||
elif [ -n "${ex}" ] && [ "${v}" = "${ex}" ] && echo "${key}" | grep -q "PASSWORD\|SECRET"; then
|
||||
fail "${key} jest rowny wartosci z ${EXAMPLE_FILE}"
|
||||
fi
|
||||
done
|
||||
|
||||
# TURN_PASSWORD wymagany tylko przy wlaczonym profilu wideo
|
||||
VIDEO_PROVIDER="$(value_of VIDEO_PROVIDER "${ENV_FILE}")"
|
||||
if [ "${VIDEO_PROVIDER^^}" = "LIVEKIT" ]; then
|
||||
[ -z "$(value_of TURN_PASSWORD "${ENV_FILE}")" ] \
|
||||
&& fail "TURN_PASSWORD jest pusty przy VIDEO_PROVIDER=LIVEKIT" \
|
||||
"Bez TURN czesc uczestnikow za firewallem firmowym nikogo nie uslyszy."
|
||||
[ -z "$(value_of LIVEKIT_NODE_IP "${ENV_FILE}")" ] \
|
||||
&& warn "LIVEKIT_NODE_IP jest pusty" \
|
||||
"Na serwerze publicznym ustaw tu jego adres IP, inaczej media moga sie nie zestawic."
|
||||
fi
|
||||
|
||||
# --- konfiguracja wymagana, prosto z katalogu ustawien ---
|
||||
echo
|
||||
echo "[5/5] Konfiguracja wymagana (katalog SettingKey)"
|
||||
|
||||
if [ "${SKIP_DB}" -eq 1 ]; then
|
||||
echo " pominieto (--skip-db)"
|
||||
elif ! command -v docker >/dev/null 2>&1; then
|
||||
warn "brak polecenia docker, pomijam sprawdzenie w bazie"
|
||||
else
|
||||
PORT="$(value_of FRONTEND_PORT_EXPOSED "${ENV_FILE}")"
|
||||
PORT="${PORT:-8081}"
|
||||
|
||||
# Zrodlem prawdy o tym, ktore klucze sa wymagane, jest katalog SettingKey
|
||||
# w backendzie, a nie lista przepisana do tego skryptu: przepisana
|
||||
# rozjechalaby sie z katalogiem przy pierwszym nowym kluczu.
|
||||
RESPONSE="$(curl -fsS --max-time 5 "http://127.0.0.1:${PORT}/api/health/config" 2>/dev/null || echo '')"
|
||||
|
||||
if [ -z "${RESPONSE}" ]; then
|
||||
echo " instancja nie odpowiada, pomijam"
|
||||
echo " To normalne PRZED pierwszym uruchomieniem."
|
||||
echo " Po starcie uruchom skrypt ponownie, zeby sprawdzic klucze"
|
||||
echo " ustawiane w panelu (SMTP, bramka SMS, dostawca wideo)."
|
||||
elif echo "${RESPONSE}" | grep -q '"ready":true'; then
|
||||
ok "wszystkie wymagane klucze maja wartosc"
|
||||
else
|
||||
echo "${RESPONSE}" \
|
||||
| tr '{' '\n' \
|
||||
| grep -o '"key":"[A-Z_]*"' \
|
||||
| cut -d'"' -f4 \
|
||||
| while read -r key; do
|
||||
fail "brak wymaganej wartosci: ${key}" \
|
||||
"Ustaw w panelu albo w ${ENV_FILE}"
|
||||
done
|
||||
# petla `while` biegnie w podpowloce, wiec licznik trzeba podniesc tutaj
|
||||
MISSING_COUNT="$(echo "${RESPONSE}" | grep -o '"key":"[A-Z_]*"' | wc -l)"
|
||||
ERRORS=$((ERRORS + MISSING_COUNT))
|
||||
fi
|
||||
fi
|
||||
|
||||
# --- podsumowanie ---
|
||||
echo
|
||||
echo "=========================================================="
|
||||
if [ "${ERRORS}" -gt 0 ]; then
|
||||
red " NIE URUCHAMIAJ: ${ERRORS} bledow, ${WARNINGS} ostrzezen"
|
||||
echo "=========================================================="
|
||||
echo
|
||||
echo "Popraw ${ENV_FILE} i uruchom skrypt ponownie."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "${WARNINGS}" -gt 0 ]; then
|
||||
yellow " Mozna uruchomic: 0 bledow, ${WARNINGS} ostrzezen do przejrzenia"
|
||||
else
|
||||
green " Konfiguracja w porzadku"
|
||||
fi
|
||||
echo "=========================================================="
|
||||
exit 0
|
||||
@@ -0,0 +1,122 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Odtworzenie instancji z kopii zapasowej.
|
||||
#
|
||||
# ./scripts/restore.sh ./backups/20260828-143000
|
||||
#
|
||||
# UWAGA: skrypt NADPISUJE biezaca baze i pliki MinIO. Wymaga potwierdzenia.
|
||||
# Przed odtworzeniem warto zrobic kopie stanu biezacego:
|
||||
# ./scripts/backup.sh
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
COMPOSE_FILE="${COMPOSE_FILE:-docker-compose.prod.yml}"
|
||||
ENV_FILE="${ENV_FILE:-.env.production}"
|
||||
SOURCE="${1:-}"
|
||||
|
||||
compose() {
|
||||
docker compose -f "${COMPOSE_FILE}" --env-file "${ENV_FILE}" "$@"
|
||||
}
|
||||
|
||||
log() {
|
||||
echo "[$(date +%H:%M:%S)] $*"
|
||||
}
|
||||
|
||||
if [ -z "${SOURCE}" ]; then
|
||||
echo "Uzycie: $0 <katalog-kopii>" >&2
|
||||
echo "Dostepne kopie:" >&2
|
||||
ls -1 ./backups 2>/dev/null | sed 's/^/ /' >&2 || echo " brak katalogu ./backups" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f "${SOURCE}/baza.dump" ]; then
|
||||
echo "BLAD: brak ${SOURCE}/baza.dump" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -f "${SOURCE}/INFO.txt" ]; then
|
||||
echo "--- Kopia do odtworzenia ---"
|
||||
cat "${SOURCE}/INFO.txt"
|
||||
echo "----------------------------"
|
||||
fi
|
||||
|
||||
DB_NAME="$(grep -E '^DB_NAME=' "${ENV_FILE}" | cut -d= -f2-)"
|
||||
DB_USER="$(grep -E '^DB_USER=' "${ENV_FILE}" | cut -d= -f2-)"
|
||||
|
||||
echo
|
||||
echo "UWAGA: ta operacja skasuje biezaca zawartosc bazy ${DB_NAME} i plikow MinIO."
|
||||
read -r -p "Wpisz ODTWORZ, aby kontynuowac: " CONFIRM
|
||||
if [ "${CONFIRM}" != "ODTWORZ" ]; then
|
||||
echo "Przerwano."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Backend musi byc zatrzymany, inaczej trzyma polaczenia do bazy
|
||||
# i pisze do niej w trakcie odtwarzania.
|
||||
log "Zatrzymuje backend..."
|
||||
compose stop backend
|
||||
|
||||
log "Odtwarzam baze..."
|
||||
# --clean --if-exists usuwa istniejace obiekty przed wgraniem
|
||||
compose exec -T postgres pg_restore \
|
||||
-U "${DB_USER}" \
|
||||
-d "${DB_NAME}" \
|
||||
--clean \
|
||||
--if-exists \
|
||||
--no-owner \
|
||||
< "${SOURCE}/baza.dump"
|
||||
|
||||
log "Baza odtworzona"
|
||||
|
||||
if [ -f "${SOURCE}/minio.tar.gz" ]; then
|
||||
log "Odtwarzam pliki MinIO..."
|
||||
|
||||
# Obraz MinIO nie zawiera tar, wiec rozpakowujemy z pomocniczego kontenera
|
||||
# podpietego do tego samego wolumenu. MinIO musi byc w tym czasie zatrzymane,
|
||||
# zeby nie pisalo do katalogu w trakcie podmiany.
|
||||
MINIO_CONTAINER="$(compose ps -q minio)"
|
||||
if [ -z "${MINIO_CONTAINER}" ]; then
|
||||
echo "BLAD: kontener minio nie istnieje" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
MINIO_VOLUME="$(docker inspect "${MINIO_CONTAINER}" \
|
||||
--format '{{range .Mounts}}{{if eq .Destination "/data"}}{{.Name}}{{end}}{{end}}')"
|
||||
|
||||
if [ -z "${MINIO_VOLUME}" ]; then
|
||||
echo "BLAD: nie udalo sie ustalic wolumenu MinIO" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log " wolumen: ${MINIO_VOLUME}"
|
||||
compose stop minio
|
||||
|
||||
# czyscimy katalog danych, zeby nie zostaly pliki spoza kopii
|
||||
docker run --rm -v "${MINIO_VOLUME}:/data" alpine:3.20 \
|
||||
sh -c 'rm -rf /data/..?* /data/.[!.]* /data/* 2>/dev/null || true'
|
||||
|
||||
docker run --rm -i -v "${MINIO_VOLUME}:/data" alpine:3.20 \
|
||||
tar xzf - -C /data < "${SOURCE}/minio.tar.gz"
|
||||
|
||||
log "Pliki MinIO odtworzone"
|
||||
compose start minio
|
||||
else
|
||||
log "Pominieto MinIO: brak minio.tar.gz w kopii"
|
||||
fi
|
||||
|
||||
log "Uruchamiam backend..."
|
||||
compose start backend
|
||||
|
||||
log "Czekam na gotowosc aplikacji..."
|
||||
for i in $(seq 1 60); do
|
||||
if compose exec -T backend sh -c 'wget -qO- http://localhost:8080/api/health >/dev/null 2>&1'; then
|
||||
log "Aplikacja odpowiada"
|
||||
break
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
|
||||
echo
|
||||
log "Gotowe. Sprawdz recznie:"
|
||||
echo " curl http://localhost:\${FRONTEND_PORT_EXPOSED:-8081}/api/health"
|
||||
echo " zaloguj sie do panelu i sprawdz liste webinarow"
|
||||
Reference in New Issue
Block a user