Files
akademiawodociagowca/scripts/backup.sh
T
2026-09-02 08:21:27 +02:00

165 lines
5.3 KiB
Bash

#!/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}"