165 lines
5.3 KiB
Bash
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}"
|