PostgreSQL z PgAdmin4 w Dockerze

Czerwiec 30, 2024 | #docker , #postgresql , #db

000100
000011
100000
000000

PostgreSQL jest jedną z najpopularniejszych baz danych często wykorzystywaną w projektach. Żeby praca z PostgreSQL-em w kontenerze Dockera była przyjemna i bezproblemowa warto zaopatrzyć go o kilka przydatnych narzędzi.

Cookiecutter to narzędzie open source, które umożliwia tworzenie szablonów projektów programistycznych. Jest szczególnie popularne w społeczności Pythona, choć można go używać z różnymi językami programowania i typami projektów. Jest przydatne zarówno dla doświadczonych, jak i początkujących programistów ponieważ ułatwia odpalenie nowego projektu w ustandaryzowany sposób. Jakość szablonów cookiecuttera jest różna, ale ten dla projektu Django jest przemyślany, a jedną z jego zalet jest między innymi dobrze przygotowany Dockerfile dla PostgreSQL-a, który dodaje do obrazu skrypty do zarządzania backup-ami . Od kiedy ich przydatność sprawdziłem w boju, niemal wszystkie używane przeze mnie kontenery z bazą danych są wyposażone w ich odpowiedniki.

Zazwyczaj z uwagi na wymagania projektu piszę własną wersję tych skryptów choćby po to aby backup tworzył się według innych reguł, by spełnić wymagania licencyjne, albo tworzę ich odpowiedniki dla innych silników baz danych np. MySQL. Na potrzeby tego artykułu posłużę, się oryginalnymi skryptami z wyżej wspomnianego projektu nieco tylko uproszczonymi w celu ograniczenia ilości przytaczanego kodu.

Dodam też od siebie banalny, choć przydatny skrypt ułatwiający zalogowanie się do psql-a w kontenerze Dockera bez konieczności podawania hasła, czy bazy danych, które to dane będą przechowywane w formie zmiennych środowiskowych w pliku .env. Dzięki temu szybki wgląd w dane czy to na localhoście, czy też na dowolnym innych środowisku nie będzie od developera wymagało uprzedniego i uciążliwego sprawdzenia credentiali.

Nie każdy, tak jak ja lubi używać CLI (Command-Line Interface) i nie zawsze też jest możliwość skorzystania z zewnętrznego GUI do bazy danych, dlatego kolejnym narzędziem, jakim niekiedy warto wzbogacić konfigurację projektu jest jakiś graficzny menadżer baz danych, który także da się zamknąć w kontenerze Dockera. PgAdmin 4 udostępnia intuicyjny webowy interface dla PostgreSQL-a, udostępnia zaawansowane funkcje i świetnie sprawdzi się nie tylko na localhoście, ale też w środowiskach testowych czy developerskich. Zaproponowana przeze mnie konfiguracja także nastawiona jest na wygodę pracy - czyli brak konieczności autoryzowania się użytkownikiem i hasłem do bazy danych, dlatego też jej użycie wymaga zwrócenia uwagi na to czy dostęp do PgAdmina 4 - na środowisku innym niż localhost - nie jest ograniczony w jakiś inny sposób - VPN, htpasswd czy cokolwiek innego.

Nikt nie tworzy konfiguracji projektu w docker-compose.yml złożonego z samej bazy danych, gdyż stanowi ona raczej zaplecze dla innego kontenera, jednak w tym artykule pominę kontenery aplikacji zakładając, że wiesz jak połączyć je z bazą danych w Dockerze, a skupię się jedynie na aprowizacji samego obrazu PostgreSQL-a i customizacji obrazu PgAdmina. Traktuj zatem poniższą strukturę jako wycinek całego projektu

Struktura omawianych plików wygląda następująco.

├── backups
├── docker
│   ├── pgadmin
│      ├── Dockerfile
│      └── entrypoint.sh
│   └── postgres
│       ├── maintenance
│	       ├── _sourced
│	          ├── constants.sh
│             └── messages.sh
│	       ├── backup
│	       ├── backups
│	       ├── cli
│	       ├── restore
│          └── rmbackup
│       └── Dockerfile
├── .env
└── docker-compose.yml

Plik konfiguracyjny Docker Compose docker-compose.yml definiuje dwa serwisy: postgres i pgadmin.

version: '3.8'
  
services: 
  
  postgres:
    build:
      dockerfile: ./docker/postgres/Dockerfile
      context: . 
    command: postgres -c log_statement=all  
    volumes:  
      - postgres-volume:/var/lib/postgresql/data
      - ./backups:/backups
    ports:  
      - "5433:5432"  
    env_file: ./.env  
  
  pgadmin:  
    build:  
      dockerfile: ./docker/pgadmin/Dockerfile  
      context: .  
    image: sls.pgadmin4:latest  
    ports:  
      - 8081:80  
    depends_on:  
      - postgres  
    volumes:  
      - pgadmin-volume:/var/lib/pgadmin  
    environment:
      - PGADMIN_DEFAULT_EMAIL=postgres@example.com
      - PGADMIN_DEFAULT_PASSWORD=postgres
      - PGADMIN_CONFIG_SERVER_MODE=False
      - PGADMIN_CONFIG_MASTER_PASSWORD_REQUIRED=False  
    env_file: ./.env  

volumes:  
  postgres-volume:  
  pgadmin-volume:

PgAdmin 4 wymaga do działania PostgreSQL-a co obrazuje parametr depends_on. Oba serwisy współdzielą zmienne środowiskowe zdefiniowane w pliku .env przechowujące w tym wypadku dane definiujące bazę danych oraz dostęp do niej.

# PostgreSQL  
# ---------------------------  
POSTGRES_DB=dbname  
POSTGRES_USER=CHANGE_ME  
POSTGRES_PASSWORD=CHANGE_ME  
POSTGRES_HOST=postgres  
POSTGRES_PORT=5432

Oba serwisy mają zdefiniowane wolumeny nazwane przeznaczone na przechowywane dane. Serwis postgres ma ponadto podmontowany katalog hosta backups, który jest miejscem przeznaczonym na przechowywanie kopi zapasowych, do których niedługo przejdziemy.

Oba serwisy budowane są na podstawie plików Dockerfile

Dockerfile dla PostgreSQL

Plik Dockera przeznaczony dla PostgreSQL ./docker/postgres/Dockerfile jest prosty i nie zmienia nic w domyślnym obrazie poza dodaniem kilku przydatnych skryptów.

FROM docker.io/postgres:16.1-alpine3.19

COPY ./docker/postgres/maintenance /usr/local/bin/maintenance
RUN chmod +x /usr/local/bin/maintenance/* \
    && cp -r /usr/local/bin/maintenance/* /usr/local/bin/ \
    && rm -rf /usr/local/bin/maintenance

Skrypty do zarządzania kopiami bazy danych

Jak już wspomniałem wyżej, skrypty do zarządzania kopiami bazy danych zaczerpnięte są z jednego z szablonów cookiecutter-a. Usunąłem z nich część komentarzy i zmieniłem hashbang z #!/usr/bin/env bash na #!/usr/bin/env sh, jako że użyłem obrazu Dockera w wersji alpine, która nie zawiera Bash-a.

Skrypty pomocnicze

Na początek dwa skrypty pomocnicze importowane i używane we właściwych skryptach. Nie są wywoływane bezpośrednio.

Plik ./docker/postgres/maintenance/_sourced/constants.sh - zawiera definicje dwóch stałych

#!/usr/bin/env sh

BACKUP_DIR_PATH='/backups'
BACKUP_FILE_PREFIX='backup'

Plik ./docker/postgres/maintenance/_sourced/messages.sh - zawiera z kolei funkcje pomocnicze służące do wyświetlania odpowiednio sformatowanych komunikatów.

#!/usr/bin/env sh

message_newline() {
    echo
}

message_debug()
{
    echo -e "DEBUG: ${@}"
}

message_welcome()
{
    echo -e "\e[1m${@}\e[0m"
}

message_warning()
{
    echo -e "\e[33mWARNING\e[0m: ${@}"
}

message_error()
{
    echo -e "\e[31mERROR\e[0m: ${@}"
}

message_info()
{
    echo -e "\e[37mINFO\e[0m: ${@}"
}

message_suggestion()
{
    echo -e "\e[33mSUGGESTION\e[0m: ${@}"
}

message_success()
{
    echo -e "\e[32mSUCCESS\e[0m: ${@}"
}

Skrypt do tworzenia kopii zapasowej bazy danych.

Plik ./docker/postgres/maintenance/backup służy do tworzenia kopi zapasowej bazy danych. Nie przyjmuje żadnych parametrów, a wynikiem jego wywołania powinno być utworzenie pliku kopi bazy danych w formacie sql i zarchiwizowanej gzip-em w katalogu ./backups np. ./backups/backup_2024_06_30T13_43_59.sql.gz

#!/usr/bin/env sh

set -o errexit
set -o pipefail
set -o nounset

working_dir="$(dirname ${0})"
source "${working_dir}/_sourced/constants.sh"
source "${working_dir}/_sourced/messages.sh"

message_welcome "Backing up the '${POSTGRES_DB}' database..."

if [[ "${POSTGRES_USER}" == "postgres" ]]; then
    message_error "Backing up as 'postgres' user is not supported. Assign 'POSTGRES_USER' env with another one and try again."
    exit 1
fi

export PGHOST="${POSTGRES_HOST}"
export PGPORT="${POSTGRES_PORT}"
export PGUSER="${POSTGRES_USER}"
export PGPASSWORD="${POSTGRES_PASSWORD}"
export PGDATABASE="${POSTGRES_DB}"

backup_filename="${BACKUP_FILE_PREFIX}_$(date +'%Y_%m_%dT%H_%M_%S').sql.gz"
pg_dump | gzip > "${BACKUP_DIR_PATH}/${backup_filename}"

message_success "'${POSTGRES_DB}' database backup '${backup_filename}' has been created and placed in '${BACKUP_DIR_PATH}'."

Wywołanie wymaga uruchomionego serwera baz danych więc wywołanie tylko docker compose run --rm postgres backup nie zadziała ponieważ komenda backup zastąpi domyślnie wywoływaną postgres -c log_statement=all (patrz docker-compose.yml - na marginesie dodam też, że wyświetlanie wszystkich logów PostgreSQL-a w trakcie dewelopmentu czasem się przydaje). W pierwszej kolejności należy uruchomić bazę danych (np. docker compose up -d postgres) i wykonać tworzenie kopi zapasowej z poziomu wcześniej uruchomionego kontenera

docker compose exec -it postgres backup

Skrypt do wyświetlenia listy utworzonych kopii zapasowych bazy danych

Plik ./docker/postgres/maintenance/backups służy do wyświetlania listy utworzonych backupów.

#!/usr/bin/env sh

set -o errexit
set -o pipefail
set -o nounset

working_dir="$(dirname ${0})"
source "${working_dir}/_sourced/constants.sh"
source "${working_dir}/_sourced/messages.sh"

message_welcome "These are the backups you have got:"

ls -lht "${BACKUP_DIR_PATH}"

Skrypt nie wymaga uruchomienia serwera bazy danych ponieważ wyświetla po prostu zawartość katalogu ./backups

docker compose run --rm postgres backups
# or
docker compose exec -it postgres backups

Skrypt do odtwarzania bazy danych z kopii zapasowej

Plik ./docker/postgres/maintenance/restore umożliwia przywrócenie bazy danych z kopii zapasowej

#!/usr/bin/env sh

set -o errexit
set -o pipefail
set -o nounset

working_dir="$(dirname ${0})"
source "${working_dir}/_sourced/constants.sh"
source "${working_dir}/_sourced/messages.sh"


if [[ -z ${1+x} ]]; then
    message_error "Backup filename is not specified yet it is a required parameter. Make sure you provide one and try again."
    exit 1
fi
backup_filename="${BACKUP_DIR_PATH}/${1}"
if [[ ! -f "${backup_filename}" ]]; then
    message_error "No backup with the specified filename found. Check out the 'backups' maintenance script output to see if there is one and try again."
    exit 1
fi

message_welcome "Restoring the '${POSTGRES_DB}' database from the '${backup_filename}' backup..."

if [[ "${POSTGRES_USER}" == "postgres" ]]; then
    message_error "Restoring as 'postgres' user is not supported. Assign 'POSTGRES_USER' env with another one and try again."
    exit 1
fi

export PGHOST="${POSTGRES_HOST}"
export PGPORT="${POSTGRES_PORT}"
export PGUSER="${POSTGRES_USER}"
export PGPASSWORD="${POSTGRES_PASSWORD}"
export PGDATABASE="${POSTGRES_DB}"

message_info "Dropping the database..."
dropdb "${PGDATABASE}"

message_info "Creating a new database..."
createdb --owner="${POSTGRES_USER}"

message_info "Applying the backup to the new database..."
gunzip -c "${backup_filename}" | psql "${POSTGRES_DB}"

message_success "The '${POSTGRES_DB}' database has been restored from the '${backup_filename}' backup."

Podobnie jak w przypadku tworzenia zrzutu bazy danych, odtworzenie wymaga uruchomionego serwera bazy danych i wskazania, który backup ma zostać użyty do odtworzenia.

docker compose exec -it postgres restore <file_name>
# eg. 
# docker compose exec -it postgres restore backup_2024_06_30T13_43_59.sql.gz

Skrypt usuwający kopię bazy danych

Plik ./docker/postgres/maintenance/rmbackup ułatwia usunięcie pliku kopii bazy danych. Napisałem ułatwia ponieważ plik w podmontowanym katalogu ./backups można po prostu usunąć jak każdy inny plik.

#!/usr/bin/env sh

set -o errexit
set -o pipefail
set -o nounset

working_dir="$(dirname ${0})"
source "${working_dir}/_sourced/constants.sh"
source "${working_dir}/_sourced/messages.sh"

if [[ -z ${1+x} ]]; then
    message_error "Backup filename is not specified yet it is a required parameter. Make sure you provide one and try again."
    exit 1
fi
backup_filename="${BACKUP_DIR_PATH}/${1}"
if [[ ! -f "${backup_filename}" ]]; then
    message_error "No backup with the specified filename found. Check out the 'backups' maintenance script output to see if there is one and try again."
    exit 1
fi

message_welcome "Removing the '${backup_filename}' backup file..."

rm -r "${backup_filename}"

message_success "The '${backup_filename}' database backup has been removed."

Wywołanie

docker compose run --rm postgres rmbackup <file_name>
# or
docker compose exec -it postgres rmbackup <file_name>
# eg.
# docker compose run --rm postgres rmbackup backup_2024_06_30T13_43_59.sql.gz

Skrypt ułatwiający logowanie do konsoli psql

Pliku ./docker/postgres/maintenance/cli nie znajdziecie w cookiecutterze - przynajmniej na czas pisania tego artykułu, a ułatwia on życie bo zamiast wywoływać:

docker compose exec -it postgres psql -U <user_name> -d <db_name>

wystarczy wywołać:

docker compose exec -it postgres cli

Niby drobna zmiana, ale jak wchodzisz na jeden z kilku serwerów testowych, na których są losowo generowane credentiale i chcesz coś szybko sprawdzić w BD i oczywiście nie pamiętasz jak się nazywa użyty akurat tu user i nazwa bazy, to - wierz mi - docenisz to usprawnienie.

#!/usr/bin/env sh

set -o errexit
set -o pipefail
set -o nounset

psql -U "${POSTGRES_USER}" -d "${POSTGRES_DB}"

Dockerfile dla PgAdmin 4

Dodanie PgAdmin 4, Adminer-a itp. do konfiguracji Docker Compose to prosta sprawa i znajdziesz w internecie wiele przepisów na to jak to zrobić prosto, ale zwykle są to wskazówki ograniczające się do absolutnego minimum. W efekcie dostajesz narzędzie do którego musisz się za każdym razem logować zwykle wskazując serwer i użytkownika bazy danych, a w przypadku Adminer-a dodatkowo jeszcze silnik bazy danych. Naturalnie jeśli Ci to nie przeszkadza to nie ma o czym mówić, ale jeśli na localhoście uważasz to za zbędne operacje i chciałbyś mieć szybki wgląd w to co się dzieje w twojej roboczej bazie danych to zapoznaj się z zaproponowanym przeze mnie plikiem ./docker/pgadmin/Dockerfile :

FROM dpage/pgadmin4:8.9
  
USER root  
  
RUN mv /entrypoint.sh /entrypoint.orig.sh  
COPY ./docker/pgadmin/entrypoint.sh /entrypoint.sh  
RUN chmod +x /entrypoint.sh \
    && touch /pgadmin4/servers.json \
    && chown pgadmin:root /pgadmin4/servers.json \  
    && touch /pgpass 
    && chown pgadmin:root /pgpass  
  
USER pgadmin

ENTRYPOINT [ "/entrypoint.sh" ]

Dockerfile zmienia nazwę oryginalnemu skryptowi startowemu, po czym kopiuje w jego miejsce nowy skrypt ./docker/pgadmin/entrypoint.sh.

#!/bin/sh

cat > /pgadmin4/servers.json <<EOF
{
  "Servers": {
    "1": {
      "Group": "Servers",
      "Name": "Local Postgres",
      "Host": "${POSTGRES_HOST}",
      "Port": 5432,
      "MaintenanceDB": "${POSTGRES_DB}",
      "Username": "${POSTGRES_USER}",
      "PassFile": "/pgpass",
      "SSLMode": "prefer"
    }
  }
}
EOF

echo "${POSTGRES_HOST}:${POSTGRES_PORT}:${POSTGRES_DB}:${POSTGRES_USER}:${POSTGRES_PASSWORD}" > /pgpass

chmod 600 /pgpass

sh /entrypoint.orig.sh

Skrypt ten musi zostać wywołany przed odpaleniem tego pierwotnego entrypoint.orig.sh, ponieważ generuje on konfigurację, która pozwoli uruchamiać PdAdmin-a bez autoryzacji i z automatycznym logowaniem się do roboczej bazy danych PostgreSQL.

Nowy skrypt startowy wymaga istnienia dwóch plików /pgadmin4/servers.json i /pgpass więc są one tworzone i są im nadawane stosowne uprawnienia w chwili budowania obrazu.

Plik servers.json w PgAdmin 4 służy do przechowywania konfiguracji serwerów PostgreSQL, do których można się łączyć za pomocą tej aplikacji. Wykorzystywany jest przez PgAdmin do załadowania listy serwerów, które są dostępne dla użytkownika, gdy uruchamia aplikację. Dzięki temu użytkownik nie musi ręcznie konfigurować połączeń za każdym razem, gdy uruchamia PgAdmin. Plik servers.json może być edytowany ręcznie, ale zazwyczaj jest tworzony i aktualizowany przez samą aplikację PgAdmin, gdy użytkownik dodaje lub modyfikuje konfiguracje serwerów w interfejsie użytkownika.

Plik pgpass w PostgreSQL służy do automatycznego zarządzania hasłami do baz danych, aby użytkownik nie musiał ich podawać ręcznie przy każdym połączeniu. Jest to plik tekstowy, który zawiera informacje o połączeniach, takie jak host, port, nazwa bazy danych, nazwa użytkownika oraz hasło. Kiedy klient PostgreSQL, taki jak psql, pg_dump, czy jak w tym przypadku PgAdmin 4 łączący się z bazą danych PostgreSQL, próbuje nawiązać połączenie, sprawdza ten plik, aby znaleźć odpowiednie hasło dla danego połączenia. Jeśli odpowiednie dane znajdują się w pgpass, połączenie zostanie nawiązane bez potrzeby ręcznego wprowadzania hasła.

Na koniec trzeba jeszcze wyjaśnić znaczenie przekazywanych do kontenera PgAdmin-a za pośrednictwem pliku docker-compose.yml zmiennych środowiskowych.

  • PGADMIN_DEFAULT_EMAIL i PGADMIN_DEFAULT_PASSWORD: Używane są do automatycznego tworzenia konta administratora przy pierwszym uruchomieniu, ale w naszym przypadku nie mają większego znaczenia poza tym, że muszą być zdefiniowane bo inaczej kontener z PgAdminem się wysypie przy inicjalizacji.
  • PGADMIN_CONFIG_SERVER_MODE: Określa tryb działania PgAdmin 4 (lokalny lub serwerowy). Ustawienie tej zmiennej na False powoduje zmianę trybu działania PgAdmina na lokalny co wyłącza szereg mechanizmów autoryzacji i rozróżniania użytkowników gdyż, zakłada się, że w tym trybie z PgAdmina korzysta tylko jeden, lokalny użytkownik. Pomimo tego ustawienia wciąż konieczne jest zdefiniowanie pozostałych wymienionych na tej liście zmiennych środowiskowych
  • PGADMIN_CONFIG_MASTER_PASSWORD_REQUIRED: Decyduje, czy PgAdmin wymaga konfiguracji hasła głównego. Nawet jak ta zmienna zostanie ustawiona na False i tak trzeba podać PGADMIN_DEFAULT_EMAIL i PGADMIN_DEFAULT_PASSWORD

PgAdmin 4 przy pierwszym uruchomieniu zapisuje całą swoją konfigurację w wolumenie co zajmuje trochę czasu. Przy kolejnych uruchomieniach jest już dużo szybciej. Po zmianie konfiguracji najlepiej usunąć wolumen gdyż samo przebudowanie kontenera nie wystarczy.

Cały kod zaprezentowany w artykule dostępny jest do pobrania z repozytorium github PostgreSQL with PgAdmin 4

Akceptuję Ta strona zapisuje niewielkie pliki tekstowe, nazywane ciasteczkami (ang. cookies) na Twoim urządzeniu w celu lepszego dostosowania treści oraz dla celów statystycznych. Możesz wyłączyć możliwość ich zapisu, zmieniając ustawienia Twojej przeglądarki. Korzystanie z tej strony bez zmiany ustawień oznacza zgodę na przechowywanie cookies w Twoim urządzeniu.