Articles tagged "sql"

Docker-Gitea-Postgres Part 3: Postgres Upgrade

Teil 3 einer Reihe über Gitea & Postgres im Docker-Container: Postgres Upgrade

Hintergrund dieser Reihe ist das notwendige Update einer Docker-Gitea-Instanz und in diesem Rahmen auch das Major-Version-Upgrade der zugehörigen Postgres-Datenbank.

Postgres

Major-Versionen, interne Struktur

Bis einschließlich zur Version 9 waren die zweistelligen Versionsnummern (die letzte Version war 9.6) die Haupt- oder "Major"-Versionen von Postgres. Dass die zweite Stelle im Allgemeinen bzw. bei vielen Projekten als "Minor"-Version bezeichnet wird, lief dem entgegen. Trotzdem war beim Sprung 9.5 auf 9.6 ein entsprechendes Vorgehen notwendig, da sich die interne Datenstruktur geändert hat (bzw. ändern hätte können) und die neuen Version nicht mit den alten Dateien auf der Platte umgehen konnte.

Ab der Version 10 sind jetzt auch bei Postgres Major-Versionssprünge an der ersten Stelle erkennbar, z.B. von 10 auf 11.

Die letzten Sprünge, an denen ein Upgrade nicht einfach durchgeführt werden konnte, waren folglich: 9.4 → 9.5 → 9.6 → 10 → 11 → 12 → 13 → 14

Welche Postgres-Versionen noch supported werden, kann man auf der "Versioning Policy"-Seite der Postgres-Dokumentation nachsehen.

Upgrade mit "pg_upgrade"

Seit einiger Zeit bringt Postgres ein Tool namens pg_upgrade mit, welches den alten und etwas aufwändigen Weg nicht mehr notwendig macht. Dieses Command ist allerdings auf einen Betrieb des Postgres-Servers auf einer "normalen" Linux-Installation (Host, VM, OS-Container, ...) ausgelegt und kann hier nicht helfen.

Als Alternative stehen allerdings mehrere Scripte, Tools und Docker-Container zur Verfügung, welche dieses Upgrade automagisch durchführen sollen (können). Da ich aber sowieso ein Backup der Datenbank machen möchte, bediene ich mich aus dem Skript des zweiten Teils dieser Reihe

Klassisches Upgrade

Das klassische Upgrade entspricht einem Backup, einem Upgrade des Postgres-Server und dem Restore. Die entsprechenden Schritte sind im zweiten Teil dieser Reihe (Postgres Backup) beschrieben.

Upgrade mit Docker

Ich habe mich inzwischen aus Sicherheitsgründen entschieden, das "alte" Daten-Docker-Volume erstmal aufzuheben und für die neue Postgres-Version ein neues Docker-Volume anzulegen. Erinnerung: Die neue Postgres-Version kann mit dem on-disk-format der alten Version leider nichts anfangen bzw. sie öffnen.

Folgende Schritte sind dann notwendig:

  • Den Gitea-Container stoppen, damit keine Schreibzugriffe (und später auch Lesezugriffe) auf die Datenbank kommen
  • Ein neues Volume (hier: gitea_db_14data, die 14 kommt von der Postgres-Major-Version) anlegen (lassen) - z.B. durch Anpassung im docker-compose.yml oder Ansible-Playbook oder wie auch immer die Container(-Volumes) angelegt werden/wurden.
  • Den Postgres-Container bzw. den Tag/Version im docker-compose.yml, Ansible-Playbook, ... entsprechend ändern und anlegen (lassen). In einem Fall habe ich ein Upgrade von 9-alpine auf 14-alpine durchgeführt und es hat problemlos funktioniert.
  • Den Datenbank-Container (und nur diesen) starten. Im Zweifelsfall beide Container starten und gitea wieder stoppen.
  • Anhand des zweiten Teiles dieser Reihe, Abschnitt "Restore im Docker" die Backup-Datei in den Container kopieren und wieder in die Postgres einspielen.
  • Den Gitea-Container wieder starten
  • Wen man sicher ist, dass es nicht mehr braucht wird, das Volume gitea_db_data (das alte Volume! Ohne "14"!) löschen.

Docker-Gitea-Postgres Part 2: Postgres Backup

Teil 2 einer Reihe über Gitea & Postgres im Docker-Container: Postgres Backup

Hintergrund dieser Reihe ist das notwendige Update einer Docker-Gitea-Instanz und in diesem Rahmen auch das Major-Version-Upgrade der zugehörigen Postgres-Datenbank.

Postgres

Backup allgemein

Um aus einer laufenden Postgres-Instanz (nahezu) alle Daten zu sichern, gibt es den Befehl pg_dumpall. Da das entstehende Backup reiner Text ist, wird es gleich in einem Rutsch mit xz komprimiert, damit es kleiner wird.

postgres@db:~ # pg_dumpall | xz >pg-backup.xz
[...]

Umgekehrt kann man auch einen Dump in eine (leere) Datenbank wieder einspielen :

postgres@db:~ # xzcat pg-backup.xz | psql -U <USERNAME> -d <DATABASE>

Backup im Docker

Da in den meisten Docker-Umgebungen nicht nur eine Postgres-Datenbank laufen wird, habe ich mir im Laufe der Zeit ein Script geschrieben, welches bei Übergabe des Container-Namens sich ein Backup auf den Host (konkret: ins aktuelle Verzeichnis) zieht:

root@docker:~ # .../postgres-backup.sh gitea_db
-rw-r--r-- 1 root root   115320 Apr 17 09:59 gitea_db_2022-04-17_09:59:23_postgres:9-alpine_9.6.24.xz

Im Backup-Dateinamen sind nacheinander Container-Name, Zeitstempel, Image-Name und Posgres-Version hinterlegt. So kann man auch z.B. im Rahmen eines Upgrades ;) nachvollziehen, mit bzw. auf Basis welcher Version das Backup angelegt wurde.

Das Script macht folgende Schritte:

  • (Zeile 3-13) Basierend auf dem übergebenen Namen wird die Container-ID ermittelt
  • (Zeile 14-19) Beginnt der zu Grunde liegende Image-Name nicht mit "postgres" wird das Script abgebrochen - es ist vermutlich kein Postgres-Container
  • (Zeile 21-29) Der Postgres-Username und -Version werden aus den Container-Umgebungsvariablen extrahiert
  • (Zeile 31-36) Der Backup-Dateinamen wird generiert und sichergestellt, dass es nicht bereits existiert
  • (Zeile 37-39) pg_dumpall wird aufgerufen, das Ergebnis durch xz komprimiert und ein ls -l auf die Datei angezeigt
#!/bin/ash

PGCONTAINER=$1
PGCONTID=$(docker ps -qf "name=^${PGCONTAINER}$")

if [ -z "$PGCONTID" ]; then
    echo "Container '${PGCONTAINER}' not found"
    exit 1
elif [ $(echo ${PGCONTID}| wc -m) -ne 13 ]; then
    echo "Unknown container id: ${PGCONTID}"
    exit 1
fi

DIMAGE=$(docker inspect ${PGCONTID} | jq '.[].Config.Image' | tr -d '"')
PGIMAGE=$(basename $DIMAGE | grep '\(^\|/\)postgres:')
if [ -z "${PGIMAGE}" ]; then
    echo "Not a Posgres image '${DIMAGE}'"
    exit 1
fi

export $(docker inspect ${PGCONTID} | jq '.[].Config.Env' | grep '"POSTGRES_USER\|"PG_VERSION' | tr -d '"' | sed -e 's/,$//')

if [ -z "${POSTGRES_USER}" ]; then
    echo "No Posgres user found"
    exit 1
elif [ -z "${PG_VERSION}" ]; then
    echo "No Postgres version number found"
    exit 1
fi

BACKUPFILE=${PGCONTAINER}_$(date +%F_%T)_${PGIMAGE}_${PG_VERSION}
if [ -f "${BACKUPFILE}.xz" ]; then
    echo "Backupfile exists"
    exit 1
fi

docker exec -it -w /tmp ${PGCONTID} sh -c "pg_dumpall -U ${POSTGRES_USER}" | xz >${BACKUPFILE}.xz

ls -l ${BACKUPFILE}.xz

Restore im Docker

Ich habe es nicht geschafft, die Backup-Datei vom Host aus direkt an einen psql in einen Container per Pipe zu übergeben. Falls hier jemand einen Tipp hat, gerne per Mail an mich ;)

Um also die Daten in den neu angelegten (oder umgezogenen oder...) Container zu bringen, kopiert man die Backup-Datei in den Container:

root@docker:~ # docker cp $BACKUPFILE gitea_db:/tmp/

Der zweite Schritt besteht dann darin, im Container das Backup wieder an Postgres zu verfüttern:

root@docker:~ # docker exec -it gitea_db bash
root@bash-5.1# psql -U gitea -d gitea < /tmp/gitea_db_2022-04-17_09:59:23_postgres:9-alpine_9.6.24.xz
root@bash-5.1# rm /tmp/gitea_db_2022-04-17_09:59:23_postgres:9-alpine_9.6.24.xz

Es macht natürlich keinen Sinn, das Backup im Container liegen zu lassen, es ist schließlich nur eine Kopie, daher wird es auch gleich wieder gelöscht.

Anmerkungen:

  • Meine Docker-Installationen laufen im Allgemeinen auf einem btrfs-Dateisystem. Daher ist Kopieren und Löschen ein Vorgang, der keinerlei Speicher benötigt. Läuft Docker allerdings mit irgendeiner Art von Images, dann ist dieser HD-/SSD-Speicherplatz bis zum nächsten Container-Anlegen und anschließendem "purge" verloren! Hier geht's nur um ca. 100kB, also wirklich nicht viel. Bei einer Multi-GiByte-Datenbank sieht das dann vielleicht anders aus!
  • Natürlich könnte man die Backup-Datei in ein temporäres Docker-Volume kopieren, dieses in den neuen Datenbank-Container einhängen und von dort das Restore machen. Der Aufwand war/ist mir aber zu groß.
  • Wer sofort seinen Speicher zurück möchte, der kann natürlich nach dem Restore den Container zerstören und neu anlegen. Dann ist der Speicher nach der nächsten Maintainance auch wieder frei.