Skip to content

feat(dbserver): migrate to stock postgres image + bind-mounted autotune#10

Merged
mpasternak merged 15 commits into
mainfrom
feat/dbserver-stock-postgres-autotune
Jun 18, 2026
Merged

feat(dbserver): migrate to stock postgres image + bind-mounted autotune#10
mpasternak merged 15 commits into
mainfrom
feat/dbserver-stock-postgres-autotune

Conversation

@mpasternak

Copy link
Copy Markdown
Member

Co i dlaczego

Obraz iplweb/bpp_dbserver jest wycofany — jego jedynym dodatkiem ponad stockowego postgresa był autotune. Ten PR przenosi dbserver na oficjalny obraz postgres:<MAJOR.MINOR> (Debian) z dwoma skryptami autotune bind-mountowanymi z repo (dbserver/, skopiowane verbatim z iplweb/bpp-dbserver). Brak buildu, brak python3.

Decyzje (uzgodnione): ICU pl-PL dla świeżych instalacji + pełny zakres (swap obrazu oraz uproszczenie flow upgrade'u majora).

Zmiany

docker-compose.database.yml

  • imagepostgres:${DJANGO_BPP_POSTGRESQL_VERSION:-${DJANGO_BPP_DBSERVER_PG_VERSION:-16.13}} (dwuwarstwowy fallback zachowany)
  • autotune entrypoint + command: postgres; skrypty bind-mount read-only
  • pin PGDATA=/var/lib/postgresql/data — stock postgres:18+ domyślnie używa innej ścieżki → bez pinu zignorowałby istniejący wolumen i zainicjował pustą bazę
  • POSTGRES_INITDB_ARGS ICU pl-PLtylko świeże instalacje, nigdy nie re-kolacjonuje istniejących klastrów
  • pg_isready healthcheck — stock postgres nie ma wbudowanego, a appserver/authserver mają depends_on: dbserver: service_healthy
  • service-level env_file — include-level env_file służy tylko interpolacji compose i nie trafia do kontenera (knoby POSTGRESQL_* autotune'a)

scripts/upgrade-postgres.sh / test-upgrade-postgres.sh

  • NEW_DBSERVER_IMAGEpostgres:<ver>; usunięty wymóg "obraz musi być wcześniej opublikowany"
  • harness testowy: stockowy obraz multi-arch (usunięte platform: + DOCKER_DEFAULT_PLATFORM → leci natywnie na Apple Silicon), mirror produkcji (mounty autotune, pin PGDATA, healthcheck)

init-configs.sh / check-image-versions.sh: tylko komunikaty/komentarze (logika zmiennych wersji i migracje starych nazw bez zmian).

Docs: docs/konfiguracja/postgresql.md, limity-zasobow.md (autotune czyta limit cgroup), CLAUDE.md + spec projektowy.

Kompatybilność wsteczna

  • Bez migracji .envDJANGO_BPP_POSTGRESQL_VERSION (np. 16.13) mapuje się wprost na tag postgres:16.13.
  • Istniejące wolumeny startują bez zmian (ten sam major, PGDATA przypięte do istniejącego mount).
  • Świadoma rozbieżność: nowe instalacje kolacjonują przez ICU pl-PL; istniejące wolumeny zachowują swoją kolację.

Weryfikacja

  • docker compose config (database.yml + pełny stack + wygenerowany test compose)
  • bash dbserver/autotune.sh --test (parity self-test)
  • bash -n + pre-commit (shellcheck, check-yaml, trufflehog)
  • mkdocs build --strict (brak zepsutych linków/nav)
  • Live boot postgres:16.13: healthy; shared_buffers = 498072kB (~486 MB z limitu 2 GiB cgroup, nie domyślne 128 MB); kolacja bpp | i | en_US.utf8 | pl-PL; data_directory = /var/lib/postgresql/data

Spec: docs/superpowers/specs/2026-06-13-dbserver-stock-postgres-autotune-design.md (dwukrotnie zrecenzowany; review wyłapało defekt include-level env_file, naprawiony).

🤖 Generated with Claude Code

mpasternak and others added 15 commits June 13, 2026 17:59
The custom iplweb/bpp_dbserver image is discontinued (its only delta over
stock postgres was the autotune step). Switch dbserver to the official
Debian-based postgres:<MAJOR.MINOR> image with the autotune scripts
bind-mounted from dbserver/ (copied verbatim from iplweb/bpp-dbserver).

docker-compose.database.yml:
- image -> postgres:${DJANGO_BPP_POSTGRESQL_VERSION...} (two-tier fallback kept)
- autotune entrypoint + command: postgres; scripts bind-mounted read-only
- pin PGDATA=/var/lib/postgresql/data (stock postgres:18+ defaults elsewhere
  -> would ignore the existing volume and re-init blank)
- POSTGRES_INITDB_ARGS ICU pl-PL for fresh installs (never re-collates existing)
- pg_isready healthcheck (stock postgres has none; appserver/authserver
  depend on service_healthy)
- service-level env_file (the include-level env_file is interpolation-only and
  is not injected into the container) so autotune POSTGRESQL_* knobs are honoured

upgrade-postgres.sh / test-upgrade-postgres.sh:
- NEW_DBSERVER_IMAGE -> postgres:<ver>; drop the "wait for image publish" step
- test harness uses the stock multi-arch image (drop platform: +
  DOCKER_DEFAULT_PLATFORM -> runs native on Apple Silicon) and mirrors prod
  (autotune mounts, PGDATA pin, healthcheck)

init-configs.sh / check-image-versions.sh: message/comment updates only
(version-var logic and old-name migrations unchanged).

Docs: postgresql.md, limity-zasobow.md (autotune reads the cgroup limit),
CLAUDE.md, plus the design spec under docs/superpowers/specs/.

Verified: compose config, autotune --test, shellcheck, mkdocs --strict, and a
live boot of postgres:16.13 (healthy; shared_buffers sized to the cgroup limit;
ICU pl-PL collation; PGDATA pinned).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…collation

Clusters created by the discontinued iplweb/bpp_dbserver image bake
lc_messages/lc_monetary/lc_numeric/lc_time and datcollate/datctype =
pl_PL.utf-8, so they cannot start on the stock postgres image (which lacks the
pl_PL.UTF-8 libc locale). Migrating such a cluster therefore requires
dump -> strip the pl_PL collation -> restore into a fresh stock cluster
(ICU pl-PL), not an in-place image swap.

Adds a 3-step toolset (+ shared lib, make targets, runbook):
- pg-collation-migrate-1-dump.sh : pg_dump -Fd of the live (old-image) cluster
- pg-collation-migrate-2-fix.sh  : pg_restore -f - | strip CREATE/ALTER/COMMENT
                                   COLLATION pl_PL + COLLATE "pl_PL" -> .sql.gz
- pg-collation-migrate-3-load.sh : dropdb/createdb (ICU) + psql -f into psql 18
- mk/database.mk: migrate-collation-{dump,fix,load} targets
- docs/eksploatacja/migracja-collation-stock-pg.md: runbook

Pairs with bpp migration 0443_drop_pl_PL_collation.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…e detour

Krok 1 zrzucał format katalogowy (-Fd) + tar, co zmuszało krok 2 do
konwersji `pg_restore -f -` (binarny toc.dat) → SQL, tylko po to żeby go
zesed-ować. Format binarny nic tu nie dawał: kolację trzeba wyciąć z
TEKSTU definicji widoków (COLLATE "pl_PL"), a load i tak idzie psql-em
(jednowątkowo), więc równoległość pg_restore -j była martwa.

Teraz plain SQL przez cały pipeline:
- krok 1: `pg_dump -Fp | gzip` → db-backup-<TS>.sql.gz (in-container dump,
  host-side gzip; trap czyści .partial gdy pg_dump padnie; gzip -t)
- krok 2: `gunzip | sed | gzip` — bez docker run, bez pg_restore, bez
  obrazu postgres, bez tar; guard odrzuca stary .tar.gz; gzip -t na wejściu
- krok 3: bez zmian

Usunięto PG_TARGET_IMAGE z lib (był tylko do konwersji w kroku 2).
Makefile: migrate-collation-fix TARBALL= → DUMPGZ=. Runbook zaktualizowany.

Zweryfikowane na realnym prod-dumpie: 7 ref. kolacji → 0, ostrzeżenie
plpython działa, guard .tar.gz odrzuca stary format. shellcheck Passed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nd-to-end

Bug z produkcji: load padał na "could not create locale pl_PL.utf8", bo
realna kolacja nazywa sie `public.pl_pl` (male litery, bez cudzyslowu,
locale='pl_PL.utf8'), a sed wycinal tylko case-sensitive `pl_PL`. To samo
tlumaczy czemu migracja bpp 0443 (DROP COLLATION ... "pl_PL") jej nie
ruszyla. Teraz wzorzec uzywa klas znakowych [pP][lL]_[pP][lL] + opcjonalny
public./cudzyslow -> lapie KAZDY wariant (pl_pl, pl_PL, "pl_PL", public.*).

Dodatkowo (prosba usera): caly pipeline bez gzipa, plain .sql end-to-end:
- krok 1: pg_dump -Fp -> db-backup-<TS>.sql (bez gzipa; kompletnosc przez
  marker 'PostgreSQL database dump complete' zamiast gzip -t; pv zostaje)
- krok 2: sed in->out (bez gunzip/gzip); weryfikacja plain grep; usuwa tez
  naglowkowe komentarze '-- Name: pl_PL...; Type: COLLATION'
- krok 3: psql < file (bez gunzip), opcjonalny pasek pv; weryfikacja kolacji
  liczona case-insensitive w schemacie public
- Makefile: DUMPGZ->DUMPSQL, SQLGZ->SQL; guardy odrzucaja .gz/.tar.gz

SC2034 (NOINPUT uzywane w confirm() z lib) wyciszone tez w kroku 3 — przy
commicie lib nie jest stage'owana, wiec shellcheck nie sledzi source.

Zweryfikowane na fixturze z realnym ksztaltem dumpu: 6 ref. kolacji
(pl_pl/"pl_PL"/COLLATE oba) -> 0, string '0443_drop_pl_PL_collation' w
django_migrations zachowany. shellcheck Passed (standalone, per plik).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Realny prod-dump ma nazwe kolacji `public."pl_PL.utf8"` — identyfikator z
kropka i `.utf8` W SRODKU cudzyslowu (musi byc cytowany). Poprzedni wzorzec
`"?pl_PL"?` wymagal spacji/cudzyslowu zaraz po `pl_PL`, wiec `.utf8` go
omijal -> CREATE/ALTER COLLATION przechodzily, load padal.

Wzorzec nazwy = (public.)? ( "pl_PL<dowolny-sufiks>"  |  goly pl_pl ):
  QNAME='"[pP][lL]_[pP][lL][^"]*"'   # "pl_PL.utf8" / "pl_PL.UTF-8" / "pl_PL"
  BNAME='[pP][lL]_[pP][lL]'          # pl_pl (niecytowany ident nie ma kropek)
Oddzielne galezie quoted/unquoted chronia przed zjedzeniem przecinka w
`COLLATE pl_pl, y` (goly ident konczy sie po 5 znakach, nie lyka `,`).

Zweryfikowane na fixturze: "pl_PL.utf8"/"pl_PL"/pl_pl + 3 formy COLLATE -> 0;
przecinek w `(x COLLATE public.pl_pl), y` zachowany; '0443_drop_pl_PL_collation'
w django_migrations i string-mention 'pl_PL' nietkniete. shellcheck Passed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ys collation-only

Load do swiezego klastra padal na "role bpp does not exist": per-bazowy
pg_dump nie zrzuca rol (sa cluster-level), a swiezy psql 18 ma tylko
superusera POSTGRES_USER (np. postgres), nie ma `bpp` z OWNER TO.

Rozwiazanie strukturalne i bezpieczne: krok 1 robi teraz
`pg_dump --no-owner --no-privileges` — zrzut bez OWNER TO / GRANT / REVOKE,
wiec laduje sie do dowolnego klastra (obiekty obejmuje user ladujacy).
Pojedynczy pg_dump i tak nie tworzy userow (CREATE ROLE to pg_dumpall).

Krok 2 NIE strippuje ownerow/grantow sed-em: rozwazane, ale `^GRANT`/
`^OWNER TO` potrafi wystapic w ciele funkcji (dollar-quote) i line-based sed
by je uszkodzil. Owner-stripping nalezy do dump-time (--no-owner), nie do
post-processingu. Krok 2 zostaje czysto kolacyjny.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…--pick

restore.sh dzialal tylko na parach db+media w formacie -Fd (.tar.gz) i
globowal *.tar.gz, wiec plain .sql/.sql.gz nie dalo sie ani podac, ani
wybrac przez --pick.

Nowosci:
- detekcja formatu zrzutu DB (auto): -Fd tar.gz (toc.dat) -> pg_restore -Fd;
  -Fc (magic PGDMP), tez gzipowany -> pg_restore; plain .sql / .sql.gz -> psql.
  pg_restore zawsze --no-owner --no-privileges; plain wczytywany psql-em.
- --db-file=PATH: pojedynczy plik zrzutu (dowolny format, tez spoza katalogu
  backupow), tryb DB-only (media nietkniete). plain/.gz/-Fc ladowane przez
  stdin (dziala dla pliku spoza /backup); -Fd kopiowany do /backup gdy trzeba,
  nazwa katalogu top-level czytana z tarballa.
- --db-format=auto|directory|custom|plain: reczny override detekcji.
- --pick pokazuje TERAZ ZAROWNO pary db+media, JAK I pojedyncze zrzuty DB
  (*.sql/*.sql.gz/*.dump/*.custom). Wybor zwraca pair:<TS> albo file:<PATH>.
- Makefile: make restore DBFILE=... [DBFORMAT=...].

Zweryfikowane: detekcja 5 formatow na fixturach (directory/custom/custom-gz/
plain/plain-gz), listing pickera (pary + pliki, bez podwojnego listowania
.tar.gz), ekstrakcja tagu, shellcheck Passed, -h OK. Sciezki dockerowe
(pg_restore/psql/tar w kontenerze) do sprawdzenia na zywym stacku.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sanity-check plpython w kroku 2 dawal falszywy alarm: `grep plpython3u|
LANGUAGE plpython` lapal slowo "plpython" w NAZWACH migracji w danych
django_migrations (0440_port_plpython_to_plpgsql, 0442_drop_plpython3u) —
czyli ostrzegal nawet na zrzutach JUZ po usunieciu plpythona.

Teraz szukamy REALNEGO uzycia, ktore faktycznie wywroci load na stocku:
  ^CREATE EXTENSION ... plpython   |   LANGUAGE plpython
Nazwy migracji (bez "CREATE EXTENSION"/"LANGUAGE " przed plpython) nie pasuja.

Zweryfikowane: post-0442 (tylko nazwy migracji) -> brak ostrzezenia;
pre-0442 (CREATE EXTENSION plpython3u + LANGUAGE plpython3u) -> ostrzega.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…st make migrate

Appserver przy starcie sam przepuszcza migracje; 'make migrate' wymaga
juz dzialajacego appservera (robi docker compose exec appserver ...).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… pisarze

Ostrzezenie/prompt 'aplikacja NIE jest zatrzymywana' bylo bezwarunkowe —
mylace, gdy stack stoi a recznie wstal tylko dbserver. Teraz skrypt sprawdza,
ktore z serwisow-pisarzy (appserver, workerserver, celerybeat, denorm-queue)
faktycznie chodza: pyta tylko o dzialajace, a gdy zaden nie dziala — leci bez
promptu (zrzut i tak spojny). Pisarzami sa tez celery/denorm-queue, nie tylko
appserver.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
make migrate robi 'docker compose exec appserver ...', wiec wymaga juz
dzialajacego appservera. Po kroku 2 appserver jest zatrzymany, a krok 7
wstawia tylko dbserver — wiec dotychczasowa kolejnosc 'make migrate' ->
'make up' wywalala sie z "service appserver is not running". make up jest
teraz pierwsze (appserver przy starcie sam przepuszcza migracje), make
migrate po nim to bezpieczne, jawne powtorzenie. Bug przezyl bo
test-upgrade-postgres.sh jawnie pomija krok 10.

Zaktualizowano tez opisy kroku 10 (naglowek skryptu, komunikaty, komentarz
testu, docs/konfiguracja/postgresql.md).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
pg_database_size to rozmiar FIZYCZNY (z indeksami, TOAST skompresowany).
Plain-SQL dump nie zrzuca danych indeksow -> realnie bywa ~40% pg_database_size,
przez co goly 'pv -s pg_database_size' konczyl pasek na ~40%. Zamiast zgadywac
funkcja katalogu (pg_table_size tez nie odda kodowania COPY ani indeksow),
kalibrujemy: po zrzucie zapisujemy ratio = rozmiar_zrzutu / pg_database_size do
$HOST_BACKUP_DIR/.pg-dump-size-ratio, a nastepny run mnozy nim estymate. Ratio
walidowane do zakresu 0.1..20. Kierunkowo-agnostyczne (dziala gdy dump < lub >
bazy). awk wymusza LC_ALL=C, bo pl_PL formatuje ulamek przecinkiem (0,400),
co psuloby zapis i walidacje ratio.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ie istnieje

'docker volume rm' na nieistniejacym wolumenie zwraca blad i przy set -e
zatrzymywal caly skrypt — a BRAK wolumenu to dokladnie stan docelowy
(swiezy cluster i tak powstanie). Usuwamy tylko gdy 'docker volume inspect'
potwierdzi istnienie; prawdziwy blad usuwania (np. volume in use) nadal
zatrzyma skrypt.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…o-prune po make up; default PG 18.4

- backup-runner uzywa domyslnie tego SAMEGO obrazu co dbserver
  (postgres:<MAJOR.MINOR>, Debian) zamiast osobnego -alpine -> wspoldzieli
  100% warstw, ~0 MB netto zamiast +~350 MB na dysku. Tryb external nadal
  alpine przez BPP_BACKUP_PG_IMAGE (ustawiany w init-configs, dosypywany do
  starych .env przez ensure-config-files). Blok command wykrywa apk vs
  apt-get, wiec dziala na obu obrazach.
- make up (a wiec i make run) sprzata Dockera (docker system prune -af) po
  udanym starcie (--wait) i PRZED pullem html2docx; wypisuje tylko ile GB
  zwolniono. Bez --volumes -> nazwane wolumeny danych sa bezpieczne.
- Domyslna wersja PostgreSQL dla NOWYCH instalacji: 16.13 -> 18.4
  (init-configs prompt + fresh .env). Safety-nety w compose (:-16.13) oraz
  migracja dla .env bez zmiennej CELOWO zostaja na 16.13 - kompatybilnosc
  wsteczna, zaden istniejacy klaster nie dostaje cichego upgrade'u majora.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@mpasternak mpasternak merged commit d7862ae into main Jun 18, 2026
5 checks passed
@mpasternak mpasternak deleted the feat/dbserver-stock-postgres-autotune branch June 18, 2026 09:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant