diff --git a/Dockerfile b/Dockerfile index 1b86c19..23dc763 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,6 +10,7 @@ RUN install --verbose --directory --owner postgres --group postgres --mode 1777 VOLUME /etc/repmgr COPY --chmod=644 --chown=postgres profile /var/lib/postgresql/.profile -# COPY --chmod=755 postgres.sh /usr/local/bin/postgres -COPY docker-entrypoint-initdb.d/* /docker-entrypoint-initdb.d/ -COPY docker-entrypoint.sh /usr/local/bin/ \ No newline at end of file + +COPY --chmod=755 entrypoint.sh /usr/local/bin/ + +ENTRYPOINT ["entrypoint.sh"] \ No newline at end of file diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh deleted file mode 100644 index b58399e..0000000 --- a/docker-entrypoint.sh +++ /dev/null @@ -1,383 +0,0 @@ -#!/usr/bin/env bash -set -Eeo pipefail - -# usage: file_env VAR [DEFAULT] -# ie: file_env 'XYZ_DB_PASSWORD' 'example' -# (will allow for "$XYZ_DB_PASSWORD_FILE" to fill in the value of -# "$XYZ_DB_PASSWORD" from a file, especially for Docker's secrets feature) -file_env() { - local var="$1" - local fileVar="${var}_FILE" - local def="${2:-}" - if [ "${!var:-}" ] && [ "${!fileVar:-}" ]; then - printf >&2 'error: both %s and %s are set (but are exclusive)\n' "$var" "$fileVar" - exit 1 - fi - local val="$def" - if [ "${!var:-}" ]; then - val="${!var}" - elif [ "${!fileVar:-}" ]; then - val="$(< "${!fileVar}")" - fi - export "$var"="$val" - unset "$fileVar" -} - -# check to see if this file is being run or sourced from another script -_is_sourced() { - # https://unix.stackexchange.com/a/215279 - [ "${#FUNCNAME[@]}" -ge 2 ] \ - && [ "${FUNCNAME[0]}" = '_is_sourced' ] \ - && [ "${FUNCNAME[1]}" = 'source' ] -} - -# used to create initial postgres directories and if run as root, ensure ownership to the "postgres" user -docker_create_db_directories() { - local user; user="$(id -u)" - - mkdir -p "$PGDATA" - # ignore failure since there are cases where we can't chmod (and PostgreSQL might fail later anyhow - it's picky about permissions of this directory) - chmod 00700 "$PGDATA" || : - - # ignore failure since it will be fine when using the image provided directory; see also https://github.com/docker-library/postgres/pull/289 - mkdir -p /var/run/postgresql || : - chmod 03775 /var/run/postgresql || : - - # Create the transaction log directory before initdb is run so the directory is owned by the correct user - if [ -n "${POSTGRES_INITDB_WALDIR:-}" ]; then - mkdir -p "$POSTGRES_INITDB_WALDIR" - if [ "$user" = '0' ]; then - find "$POSTGRES_INITDB_WALDIR" \! -user postgres -exec chown postgres '{}' + - fi - chmod 700 "$POSTGRES_INITDB_WALDIR" - fi - - # allow the container to be started with `--user` - if [ "$user" = '0' ]; then - find "$PGDATA" \! -user postgres -exec chown postgres '{}' + - find /var/run/postgresql \! -user postgres -exec chown postgres '{}' + - fi -} - -# initialize empty PGDATA directory with new database via 'initdb' -# arguments to `initdb` can be passed via POSTGRES_INITDB_ARGS or as arguments to this function -# `initdb` automatically creates the "postgres", "template0", and "template1" dbnames -# this is also where the database user is created, specified by `POSTGRES_USER` env -docker_init_database_dir() { - # "initdb" is particular about the current user existing in "/etc/passwd", so we use "nss_wrapper" to fake that if necessary - # see https://github.com/docker-library/postgres/pull/253, https://github.com/docker-library/postgres/issues/359, https://cwrap.org/nss_wrapper.html - local uid; uid="$(id -u)" - if ! getent passwd "$uid" &> /dev/null; then - # see if we can find a suitable "libnss_wrapper.so" (https://salsa.debian.org/sssd-team/nss-wrapper/-/commit/b9925a653a54e24d09d9b498a2d913729f7abb15) - local wrapper - for wrapper in {/usr,}/lib{/*,}/libnss_wrapper.so; do - if [ -s "$wrapper" ]; then - NSS_WRAPPER_PASSWD="$(mktemp)" - NSS_WRAPPER_GROUP="$(mktemp)" - export LD_PRELOAD="$wrapper" NSS_WRAPPER_PASSWD NSS_WRAPPER_GROUP - local gid; gid="$(id -g)" - printf 'postgres:x:%s:%s:PostgreSQL:%s:/bin/false\n' "$uid" "$gid" "$PGDATA" > "$NSS_WRAPPER_PASSWD" - printf 'postgres:x:%s:\n' "$gid" > "$NSS_WRAPPER_GROUP" - break - fi - done - fi - - if [ -n "${POSTGRES_INITDB_WALDIR:-}" ]; then - set -- --waldir "$POSTGRES_INITDB_WALDIR" "$@" - fi - - # --pwfile refuses to handle a properly-empty file (hence the "\n"): https://github.com/docker-library/postgres/issues/1025 - eval 'initdb --username="$POSTGRES_USER" --pwfile=<(printf "%s\n" "$POSTGRES_PASSWORD") '"$POSTGRES_INITDB_ARGS"' "$@"' - - # unset/cleanup "nss_wrapper" bits - if [[ "${LD_PRELOAD:-}" == */libnss_wrapper.so ]]; then - rm -f "$NSS_WRAPPER_PASSWD" "$NSS_WRAPPER_GROUP" - unset LD_PRELOAD NSS_WRAPPER_PASSWD NSS_WRAPPER_GROUP - fi -} - -# print large warning if POSTGRES_PASSWORD is long -# error if both POSTGRES_PASSWORD is empty and POSTGRES_HOST_AUTH_METHOD is not 'trust' -# print large warning if POSTGRES_HOST_AUTH_METHOD is set to 'trust' -# assumes database is not set up, ie: [ -z "$DATABASE_ALREADY_EXISTS" ] -docker_verify_minimum_env() { - if [ -z "$POSTGRES_PASSWORD" ] && [ 'trust' != "$POSTGRES_HOST_AUTH_METHOD" ]; then - # The - option suppresses leading tabs but *not* spaces. :) - cat >&2 <<-'EOE' - Error: Database is uninitialized and superuser password is not specified. - You must specify POSTGRES_PASSWORD to a non-empty value for the - superuser. For example, "-e POSTGRES_PASSWORD=password" on "docker run". - - You may also use "POSTGRES_HOST_AUTH_METHOD=trust" to allow all - connections without a password. This is *not* recommended. - - See PostgreSQL documentation about "trust": - https://www.postgresql.org/docs/current/auth-trust.html - EOE - exit 1 - fi - if [ 'trust' = "$POSTGRES_HOST_AUTH_METHOD" ]; then - cat >&2 <<-'EOWARN' - ******************************************************************************** - WARNING: POSTGRES_HOST_AUTH_METHOD has been set to "trust". This will allow - anyone with access to the Postgres port to access your database without - a password, even if POSTGRES_PASSWORD is set. See PostgreSQL - documentation about "trust": - https://www.postgresql.org/docs/current/auth-trust.html - In Docker's default configuration, this is effectively any other - container on the same system. - - It is not recommended to use POSTGRES_HOST_AUTH_METHOD=trust. Replace - it with "-e POSTGRES_PASSWORD=password" instead to set a password in - "docker run". - ******************************************************************************** - EOWARN - fi -} -# similar to the above, but errors if there are any "old" databases detected (usually due to upgrades without pg_upgrade) -docker_error_old_databases() { - if [ -n "${OLD_DATABASES[0]:-}" ]; then - cat >&2 <<-EOE - Error: in 18+, these Docker images are configured to store database data in a - format which is compatible with "pg_ctlcluster" (specifically, using - major-version-specific directory names). This better reflects how - PostgreSQL itself works, and how upgrades are to be performed. - - See also https://github.com/docker-library/postgres/pull/1259 - - Counter to that, there appears to be PostgreSQL data in: - ${OLD_DATABASES[*]} - - This is usually the result of upgrading the Docker image without - upgrading the underlying database using "pg_upgrade" (which requires both - versions). - - The suggested container configuration for 18+ is to place a single mount - at /var/lib/postgresql which will then place PostgreSQL data in a - subdirectory, allowing usage of "pg_upgrade --link" without mount point - boundary issues. - - See https://github.com/docker-library/postgres/issues/37 for a (long) - discussion around this process, and suggestions for how to do so. - EOE - exit 1 - fi -} - -# usage: docker_process_init_files [file [file [...]]] -# ie: docker_process_init_files /always-initdb.d/* -# process initializer files, based on file extensions and permissions -docker_process_init_files() { - # psql here for backwards compatibility "${psql[@]}" - psql=( docker_process_sql ) - - printf '\n' - local f - for f; do - case "$f" in - *.sh) - # https://github.com/docker-library/postgres/issues/450#issuecomment-393167936 - # https://github.com/docker-library/postgres/pull/452 - if [ -x "$f" ]; then - printf '%s: running %s\n' "$0" "$f" - "$f" - else - printf '%s: sourcing %s\n' "$0" "$f" - . "$f" - fi - ;; - *.sql) printf '%s: running %s\n' "$0" "$f"; docker_process_sql -f "$f"; printf '\n' ;; - *.sql.gz) printf '%s: running %s\n' "$0" "$f"; gunzip -c "$f" | docker_process_sql; printf '\n' ;; - *.sql.xz) printf '%s: running %s\n' "$0" "$f"; xzcat "$f" | docker_process_sql; printf '\n' ;; - *.sql.zst) printf '%s: running %s\n' "$0" "$f"; zstd -dc "$f" | docker_process_sql; printf '\n' ;; - *) printf '%s: ignoring %s\n' "$0" "$f" ;; - esac - printf '\n' - done -} - -# Execute sql script, passed via stdin (or -f flag of pqsl) -# usage: docker_process_sql [psql-cli-args] -# ie: docker_process_sql --dbname=mydb <<<'INSERT ...' -# ie: docker_process_sql -f my-file.sql -# ie: docker_process_sql > "$PGDATA/pg_hba.conf" -} - -# start socket-only postgresql server for setting up or running scripts -# all arguments will be passed along as arguments to `postgres` (via pg_ctl) -docker_temp_server_start() { - if [ "$1" = 'postgres' ]; then - shift - fi - - # internal start of server in order to allow setup using psql client - # does not listen on external TCP/IP and waits until start finishes - set -- "$@" -c listen_addresses='' -p "${PGPORT:-5432}" - - # unset NOTIFY_SOCKET so the temporary server doesn't prematurely notify - # any process supervisor. - NOTIFY_SOCKET= \ - PGUSER="${PGUSER:-$POSTGRES_USER}" \ - pg_ctl -D "$PGDATA" \ - -o "$(printf '%q ' "$@")" \ - -w start -} - -# stop postgresql server after done setting up user and running scripts -docker_temp_server_stop() { - PGUSER="${PGUSER:-postgres}" \ - pg_ctl -D "$PGDATA" -m fast -w stop -} - -# check arguments for an option that would cause postgres to stop -# return true if there is one -_pg_want_help() { - local arg - for arg; do - case "$arg" in - # postgres --help | grep 'then exit' - # leaving out -C on purpose since it always fails and is unhelpful: - # postgres: could not access the server configuration file "/var/lib/postgresql/data/postgresql.conf": No such file or directory - -'?'|--help|--describe-config|-V|--version) - return 0 - ;; - esac - done - return 1 -} - -_main() { - # if first arg looks like a flag, assume we want to run postgres server - if [ "${1:0:1}" = '-' ]; then - set -- postgres "$@" - fi - - if [ "$1" = 'postgres' ] && ! _pg_want_help "$@"; then - docker_setup_env - # setup data directories and permissions (when run as root) - docker_create_db_directories - if [ "$(id -u)" = '0' ]; then - # then restart script as postgres user - exec gosu postgres "$BASH_SOURCE" "$@" - fi - - # only run initialization on an empty data directory - if [ -z "$DATABASE_ALREADY_EXISTS" ]; then - docker_verify_minimum_env - docker_error_old_databases - - # check dir permissions to reduce likelihood of half-initialized database - ls /docker-entrypoint-initdb.d/ > /dev/null - - docker_init_database_dir - pg_setup_hba_conf "$@" - - # PGPASSWORD is required for psql when authentication is required for 'local' connections via pg_hba.conf and is otherwise harmless - # e.g. when '--auth=md5' or '--auth-local=md5' is used in POSTGRES_INITDB_ARGS - export PGPASSWORD="${PGPASSWORD:-$POSTGRES_PASSWORD}" - docker_temp_server_start "$@" - - docker_setup_db - docker_process_init_files /docker-entrypoint-initdb.d/* - - docker_temp_server_stop - unset PGPASSWORD - - cat <<-'EOM' - - PostgreSQL init process complete; ready for start up. - - EOM - else - cat <<-'EOM' - - PostgreSQL Database directory appears to contain a database; Skipping initialization - - EOM - fi - fi - - postgres & - repmgrd -f /etc/repmgr/repmgr.conf --pid-file /tmp/repmgrd.pid - -} - -if ! _is_sourced; then - _main "$@" -fi \ No newline at end of file diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100644 index 0000000..d7748dd --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,194 @@ +#!/usr/bin/env bash + +set -Eeo pipefail + +file_env() { + local var="$1" + local fileVar="${var}_FILE" + local def="${2:-}" + if [ "${!var:-}" ] && [ "${!fileVar:-}" ]; then + printf >&2 'error: both %s and %s are set (but are exclusive)\n' "$var" "$fileVar" + exit 1 + fi + local val="$def" + if [ "${!var:-}" ]; then + val="${!var}" + elif [ "${!fileVar:-}" ]; then + val="$(< "${!fileVar}")" + fi + export "$var"="$val" + unset "$fileVar" +} + +docker_setup_env() { + file_env 'POSTGRES_PASSWORD' +} + +docker_create_db_directories() { + local user; user="$(id -u)" + mkdir -p "$PGDATA" + chmod 00700 "$PGDATA" || : + mkdir -p /var/run/postgresql || : + chmod 03775 /var/run/postgresql || : + if [ "$user" = '0' ]; then + find "$PGDATA" \! -user postgres -exec chown postgres '{}' + + find /var/run/postgresql \! -user postgres -exec chown postgres '{}' + + fi +} + +docker_init_database_dir() { + local uid; uid="$(id -u)" + if ! getent passwd "$uid" &> /dev/null; then + local wrapper + for wrapper in {/usr,}/lib{/*,}/libnss_wrapper.so; do + if [ -s "$wrapper" ]; then + NSS_WRAPPER_PASSWD="$(mktemp)" + NSS_WRAPPER_GROUP="$(mktemp)" + export LD_PRELOAD="$wrapper" NSS_WRAPPER_PASSWD NSS_WRAPPER_GROUP + local gid; gid="$(id -g)" + printf 'postgres:x:%s:%s:PostgreSQL:%s:/bin/false\n' "$uid" "$gid" "$PGDATA" > "$NSS_WRAPPER_PASSWD" + printf 'postgres:x:%s:\n' "$gid" > "$NSS_WRAPPER_GROUP" + break + fi + done + fi + eval 'initdb --username=postgres --pwfile=<(printf "%s\n" "$POSTGRES_PASSWORD") '"$POSTGRES_INITDB_ARGS"' "$@"' + + if [[ "${LD_PRELOAD:-}" == */libnss_wrapper.so ]]; then + rm -f "$NSS_WRAPPER_PASSWD" "$NSS_WRAPPER_GROUP" + unset LD_PRELOAD NSS_WRAPPER_PASSWD NSS_WRAPPER_GROUP + fi +} + +docker_temp_server_start() { + NOTIFY_SOCKET= \ + PGUSER="${PGUSER:-postgres}" \ + pg_ctl -D "$PGDATA" \ + -o "-c listen_addresses='*' -p 5432" \ + -w start +} + +docker_temp_server_stop() { + PGUSER="${PGUSER:-postgres}" \ + pg_ctl -D "$PGDATA" -m fast -w stop +} + +pg_setup_conf() { + { + printf "\n" + printf "archive_command = '/bin/true'\n" + printf "archive_mode = on\n" + printf "hot_standby = on\n" + printf "max_wal_senders = 10\n" + printf "max_replication_slots = 10\n" + printf "shared_preload_libraries = 'repmgr'\n" + if [[ -n $POSTGRES_MAX_CONNECTIONS ]]; then + printf "\nmax_connections = ${POSTGRES_MAX_CONNECTIONS}\n" + fi + } >> "$PGDATA/postgresql.conf" +} + +pg_setup_hba_conf() { + { + printf '\n' + printf 'local all all trust\n' + printf 'local replication repmgr trust\n' + printf 'local repmgr repmgr trust\n' + printf '\n' + printf 'host replication repmgr 127.0.0.1/32 trust\n' + printf 'host repmgr repmgr 127.0.0.1/32 trust\n' + printf '\n' + printf 'host replication repmgr %s trust\n' "${POSTGRES_NETWORK:-100.64.0.0/10}" + printf 'host repmgr repmgr %s trust\n' "${POSTGRES_NETWORK:-100.64.0.0/10}" + printf '\n' + printf 'host all all all scram-sha-256' + } >> "$PGDATA/pg_hba.conf" +} + +repmgr_setup_conf() { + { + printf "node_id = %s\n" "$REPMGR_NODE_ID" + printf "node_name = %s\n" "$REPMGR_NODE_NAME" + printf "conninfo = 'host=%s dbname=repmgr user=repmgr connect_timeout=2'\n" "$REPMGR_NODE_NAME" + printf "location = '%s'\n" "$REPMGR_NODE_LOCATION" + printf "\n" + printf "data_directory = '/var/lib/postgresql/data'\n" + printf "use_replication_slots = on\n" + printf "pg_bindir = '/usr/lib/postgresql/15/bin/'\n" + printf "\n" + printf "failover = automatic\n" + printf "promote_command = '/usr/bin/repmgr standby promote -f /etc/repmgr/repmgr.conf --log-to-file'\n" + printf "follow_command = '/usr/bin/repmgr standby follow -f /etc/repmgr/repmgr.conf --log-to-file --upstream-node-id=%n'\n" + printf "\n" + printf "primary_visibility_consensus = on\n" + } > /etc/repmgr/repmgr.conf + +} + +docker_setup_env +docker_create_db_directories +if [ "$(id -u)" = '0' ]; then + exec gosu postgres "$BASH_SOURCE" +fi + +# convert to lowercase +REPMGR_NODE_ROLE="${REPMGR_NODE_ROLE,,}" + +if [[ ! -s "$PGDATA/PG_VERSION" ]]; then + repmgr_setup_conf + + if [[ -n $REPMGR_UPSTREAM ]] && [[ -z $REPMGR_NODE_ROLE ]]; then + REPMGR_NODE_ROLE="standby" + fi + + if [[ $REPMGR_NODE_ROLE != "standby" ]]; then + docker_init_database_dir + pg_setup_conf + pg_setup_hba_conf + + export PGPASSWORD="$POSTGRES_PASSWORD" + docker_temp_server_start + + psql -c "CREATE USER repmgr WITH SUPERUSER ENCRYPTED PASSWORD '${REPMGR_PASSWORD}';" + psql -c "CREATE DATABASE repmgr WITH OWNER repmgr;" + psql -c 'ALTER USER repmgr SET search_path TO repmgr, "$user", public;' + + unset PGPASSWORD + else + until /usr/lib/postgresql/15/bin/repmgr -h $REPMGR_UPSTREAM -U repmgr -f /etc/repmgr/repmgr.conf standby clone --dry-run &> /dev/null + do + echo "Upstream host not ready. Waiting for 5 minutes..." + sleep 300 + done + echo "Upstream host found..." + /usr/lib/postgresql/15/bin/repmgr -h $REPMGR_UPSTREAM -U repmgr -f /etc/repmgr/repmgr.conf standby clone + docker_temp_server_start + fi + + case "$REPMGR_NODE_ROLE" in + standby ) + /usr/lib/postgresql/15/bin/repmgr -f /etc/repmgr/repmgr.conf standby register + ;; + witness ) + until /usr/lib/postgresql/15/bin/repmgr -h $REPMGR_UPSTREAM -f /etc/repmgr/repmgr.conf witness register &> /dev/null + do + echo "Primary host not ready. Waiting for 5 minutes..." + sleep 300 + done + ;; + * ) + /usr/lib/postgresql/15/bin/repmgr -f /etc/repmgr/repmgr.conf primary register + ;; + esac + + docker_temp_server_stop +fi + +set -m + +postgres & + +sleep 10 +repmgrd -f /etc/repmgr/repmgr.conf --pid-file /tmp/repmgrd.pid + +fg %1 diff --git a/postgres.sh b/postgres.sh deleted file mode 100644 index 6348e3b..0000000 --- a/postgres.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env bash - -repmgrd -f /etc/repmgr.conf --pid-file /tmp/repmgrd.pid & - -export PATH="/usr/lib/postgresql/15/bin:${PATH}" - -exec "$@" diff --git a/repmgr.conf b/repmgr.conf deleted file mode 100644 index 9edfd66..0000000 --- a/repmgr.conf +++ /dev/null @@ -1,8 +0,0 @@ -data_directory = '/var/lib/postgresql/data' -use_replication_slots = on -pg_bindir = '/usr/lib/postgresql/15/bin/' -failover = automatic -promote_command = '/usr/bin/repmgr standby promote -f /etc/repmgr.conf --log-to-file' -follow_command = '/usr/bin/repmgr standby follow -f /etc/repmgr.conf --log-to-file --upstream-node-id=%n' -primary_visibility_consensus = on -