mirror of
https://github.com/Dispatcharr/Dispatcharr.git
synced 2026-07-17 16:50:53 +00:00
fix: run PostgreSQL as PUID/PGID user to resolve ownership conflicts (#1078)
PostgreSQL now runs as the PUID/PGID user ($POSTGRES_USER) instead of the internal postgres system user (UID 102). This fixes container startup failures when PUID/PGID is set, caused by chown permission errors on restricted filesystems (NFS root_squash, CIFS) and UID collisions with the postgres system user. Changes: - Run all PostgreSQL operations (initdb, pg_ctl, psql) as $POSTGRES_USER - Auto-detect PUID/PGID from existing data owner when not explicitly set - Validate PUID/PGID (reject zero, non-numeric values) before startup - Migrate existing data ownership with sentinel-based skip optimization - Use trust auth for local Unix sockets, md5 for network connections - Add promote_app_role() and ensure_app_database() as idempotent startup guarantees that handle fresh installs, upgrades, and PUID changes - Preserve postgres role as superuser for rollback compatibility - Centralize /data/db ownership in 02-postgres.sh (sentinel-aware) - Add integration test suite (20 scenarios) covering fresh installs, upgrades, restarts, PUID changes, UID collisions, bind mounts, modular mode, PG major upgrades, and end-to-end web UI verification
This commit is contained in:
parent
0bc4c40400
commit
52ed0fc15a
5 changed files with 1892 additions and 73 deletions
|
|
@ -156,15 +156,20 @@ echo "Starting init process..."
|
|||
# Start PostgreSQL if NOT in modular mode (using external database)
|
||||
if [[ "$DISPATCHARR_ENV" != "modular" ]]; then
|
||||
echo "Starting Postgres..."
|
||||
su - postgres -c "$PG_BINDIR/pg_ctl -D ${POSTGRES_DIR} start -w -t 300 -o '-c port=${POSTGRES_PORT}'"
|
||||
su - $POSTGRES_USER -c "$PG_BINDIR/pg_ctl -D ${POSTGRES_DIR} start -w -t 300 -o '-c port=${POSTGRES_PORT}'"
|
||||
# Wait for PostgreSQL to be ready
|
||||
until su - postgres -c "$PG_BINDIR/pg_isready -h ${POSTGRES_HOST} -p ${POSTGRES_PORT}" >/dev/null 2>&1; do
|
||||
until su - $POSTGRES_USER -c "$PG_BINDIR/pg_isready -h ${POSTGRES_HOST} -p ${POSTGRES_PORT}" >/dev/null 2>&1; do
|
||||
echo_with_timestamp "Waiting for PostgreSQL to be ready..."
|
||||
sleep 1
|
||||
done
|
||||
postgres_pid=$(su - postgres -c "$PG_BINDIR/pg_ctl -D ${POSTGRES_DIR} status" | sed -n 's/.*PID: \([0-9]\+\).*/\1/p')
|
||||
postgres_pid=$(su - $POSTGRES_USER -c "$PG_BINDIR/pg_ctl -D ${POSTGRES_DIR} status" | sed -n 's/.*PID: \([0-9]\+\).*/\1/p')
|
||||
echo "✅ Postgres started with PID $postgres_pid"
|
||||
pids+=("$postgres_pid")
|
||||
|
||||
# Unconditional startup guarantees — run on every AIO startup.
|
||||
# Each is idempotent and handles all scenarios (fresh, upgrade, restart).
|
||||
promote_app_role
|
||||
ensure_app_database
|
||||
else
|
||||
echo "🔗 Modular mode: Using external PostgreSQL at ${POSTGRES_HOST}:${POSTGRES_PORT}"
|
||||
# Wait for external PostgreSQL to be ready using pg_isready (checks actual protocol readiness)
|
||||
|
|
|
|||
|
|
@ -1,9 +1,59 @@
|
|||
#!/bin/bash
|
||||
|
||||
# Set up user details
|
||||
# NOTE: PUID/PGID values matching internal system UIDs (e.g. 102 for the
|
||||
# postgres package user) will cause that OS user/group to be renamed to
|
||||
# $POSTGRES_USER inside the container. This is cosmetic and does not affect
|
||||
# runtime behavior since all postgres operations run as $POSTGRES_USER
|
||||
# rather than the postgres system user.
|
||||
|
||||
# Auto-detect PUID/PGID from existing data when not explicitly set.
|
||||
# Avoids a cross-UID chown on upgrade, which would fail on restricted
|
||||
# filesystems (NFS root_squash, CIFS). UID/GID 0 is excluded — PostgreSQL
|
||||
# refuses to run as root. Falls through to default 1000 for new installs.
|
||||
if [ -z "${PUID+x}" ] && [ -f "${POSTGRES_DIR}/PG_VERSION" ]; then
|
||||
_data_uid=$(stat -c '%u' "${POSTGRES_DIR}/PG_VERSION")
|
||||
if [ "$_data_uid" -ne 0 ] 2>/dev/null; then
|
||||
export PUID=$_data_uid
|
||||
echo "PUID not set — defaulting to existing data owner UID: $PUID"
|
||||
fi
|
||||
fi
|
||||
if [ -z "${PGID+x}" ] && [ -f "${POSTGRES_DIR}/PG_VERSION" ]; then
|
||||
_data_gid=$(stat -c '%g' "${POSTGRES_DIR}/PG_VERSION")
|
||||
if [ "$_data_gid" -ne 0 ] 2>/dev/null; then
|
||||
export PGID=$_data_gid
|
||||
echo "PGID not set — defaulting to existing data owner GID: $PGID"
|
||||
fi
|
||||
fi
|
||||
export PUID=${PUID:-1000}
|
||||
export PGID=${PGID:-1000}
|
||||
|
||||
# Validate PUID/PGID are positive integers before any user/group operations.
|
||||
# Non-numeric values would cause useradd/groupadd to fail with confusing errors.
|
||||
if ! [[ "$PUID" =~ ^[0-9]+$ ]] || ! [[ "$PGID" =~ ^[0-9]+$ ]]; then
|
||||
echo ""
|
||||
echo "================================================================"
|
||||
echo "ERROR: PUID and PGID must be positive integers."
|
||||
echo " PUID=$PUID PGID=$PGID"
|
||||
echo " Please set valid numeric values (default: 1000)."
|
||||
echo "================================================================"
|
||||
echo ""
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# PostgreSQL refuses to run as root (UID 0). Block early — before any
|
||||
# user/group manipulation — to prevent renaming the root user/group,
|
||||
# which would break the container.
|
||||
if [ "$PUID" = "0" ] || [ "$PGID" = "0" ]; then
|
||||
echo ""
|
||||
echo "================================================================"
|
||||
echo "ERROR: PUID=0 or PGID=0 is not supported."
|
||||
echo " PostgreSQL cannot run as root (UID 0)."
|
||||
echo " Please set PUID and PGID to a non-zero value (default: 1000)."
|
||||
echo "================================================================"
|
||||
echo ""
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if group with PGID exists
|
||||
if getent group "$PGID" >/dev/null 2>&1; then
|
||||
# Group exists, check if it's named correctly (should match POSTGRES_USER)
|
||||
|
|
|
|||
|
|
@ -3,8 +3,41 @@
|
|||
# Skip internal PostgreSQL setup in modular mode (using external database)
|
||||
if [[ "$DISPATCHARR_ENV" != "modular" ]]; then
|
||||
|
||||
# Temporary migration from postgres in /data to $POSTGRES_DIR. Can likely remove
|
||||
# some time in the future.
|
||||
# Ensure the PostgreSQL socket directory is writable by the application user.
|
||||
# The package installs this owned by the postgres system user, but PostgreSQL
|
||||
# runs as $POSTGRES_USER (PUID:PGID) in AIO mode.
|
||||
if [ -d /var/run/postgresql ]; then
|
||||
chown $PUID:$PGID /var/run/postgresql
|
||||
fi
|
||||
|
||||
# Record PUID:PGID in a sentinel file so subsequent startups can skip
|
||||
# the expensive recursive chown when ownership is already correct.
|
||||
write_ownership_sentinel() {
|
||||
echo "$PUID:$PGID" > "${POSTGRES_DIR}/.owner_puid"
|
||||
chown "$PUID:$PGID" "${POSTGRES_DIR}/.owner_puid"
|
||||
}
|
||||
|
||||
# Write standard pg_hba.conf and enable network listening.
|
||||
# Local (Unix socket): trust — safe for single-app containers where only
|
||||
# authorized processes connect. Network: password required via md5.
|
||||
# Idempotent: safe to call on every startup.
|
||||
configure_pg_network() {
|
||||
local datadir="$1"
|
||||
cat > "${datadir}/pg_hba.conf" <<HBAEOF
|
||||
local all all trust
|
||||
host all all 0.0.0.0/0 md5
|
||||
host all all ::1/128 md5
|
||||
HBAEOF
|
||||
chown "$PUID:$PGID" "${datadir}/pg_hba.conf"
|
||||
# Remove any active listen_addresses setting, then append the canonical
|
||||
# value. Avoids duplicate accumulation across restarts. Only targets
|
||||
# uncommented lines; leaves initdb's default comment intact.
|
||||
sed -Ei '/^[[:space:]]*listen_addresses[[:space:]]*=/d' "${datadir}/postgresql.conf"
|
||||
echo "listen_addresses='*'" >> "${datadir}/postgresql.conf"
|
||||
}
|
||||
|
||||
# Legacy migration: move data from /data root into $POSTGRES_DIR.
|
||||
# Safe to remove once all deployments have upgraded past this layout.
|
||||
if [ -e "/data/postgresql.conf" ]; then
|
||||
echo "Migrating PostgreSQL data from /data to $POSTGRES_DIR..."
|
||||
|
||||
|
|
@ -24,7 +57,7 @@ if [[ "$DISPATCHARR_ENV" != "modular" ]]; then
|
|||
rmdir /tmp/postgres_migration
|
||||
|
||||
# Set proper ownership and permissions for PostgreSQL data directory
|
||||
chown -R postgres:postgres $POSTGRES_DIR
|
||||
chown -R $PUID:$PGID $POSTGRES_DIR
|
||||
chmod 700 $POSTGRES_DIR
|
||||
|
||||
echo "Migration completed successfully."
|
||||
|
|
@ -39,6 +72,80 @@ if [[ "$DISPATCHARR_ENV" != "modular" ]]; then
|
|||
CURRENT_VERSION=""
|
||||
fi
|
||||
|
||||
# =========================================================================
|
||||
# Existing data: ensure ownership, auth, and permissions are correct.
|
||||
# These guarantees run on EVERY startup with existing data — not just
|
||||
# upgrades. This eliminates conditional edge cases and ensures the
|
||||
# container always reaches a known-good state regardless of how the
|
||||
# data was originally created.
|
||||
# =========================================================================
|
||||
if [ -n "$CURRENT_VERSION" ] && [ -d "$POSTGRES_DIR" ]; then
|
||||
|
||||
# --- 1. Ownership reconciliation (conditional — only when needed) ---
|
||||
# Two triggers cause a recursive chown:
|
||||
# a) PG_VERSION owner doesn't match PUID (obvious mismatch)
|
||||
# b) Sentinel file (.owner_puid) missing or stale — catches partial
|
||||
# chown from a previous interrupted startup where early files
|
||||
# (including PG_VERSION) got the new owner but deeper files didn't.
|
||||
# After a successful chown, the sentinel records PUID:PGID so
|
||||
# subsequent startups skip the expensive recursive operation.
|
||||
OWNERSHIP_SENTINEL="${POSTGRES_DIR}/.owner_puid"
|
||||
CURRENT_OWNER=$(stat -c '%u' "$PG_VERSION_FILE")
|
||||
_needs_chown=false
|
||||
if [ "$CURRENT_OWNER" != "$PUID" ]; then
|
||||
_needs_chown=true
|
||||
elif [ ! -f "$OWNERSHIP_SENTINEL" ] || [ "$(cat "$OWNERSHIP_SENTINEL" 2>/dev/null)" != "$PUID:$PGID" ]; then
|
||||
# Sentinel missing or stale. Could be:
|
||||
# a) First startup with sentinel code (pre-existing data) — benign
|
||||
# b) Interrupted chown from a previous startup — needs re-chown
|
||||
# Spot-check a deeper directory to distinguish: if base/ also
|
||||
# matches PUID:PGID, ownership is likely consistent (case a).
|
||||
_deeper_check=$(stat -c '%u:%g' "${POSTGRES_DIR}/base" 2>/dev/null)
|
||||
if [ "$_deeper_check" != "$PUID:$PGID" ]; then
|
||||
_needs_chown=true
|
||||
else
|
||||
# Spot-check passed — ownership is consistent, record sentinel
|
||||
# so future startups skip the spot-check entirely.
|
||||
write_ownership_sentinel
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$_needs_chown" = true ]; then
|
||||
echo "Migrating PostgreSQL data ownership from UID $CURRENT_OWNER to $PUID:$PGID..."
|
||||
echo " This may take several minutes for large databases. Do not stop the container."
|
||||
if ! chown -R "$PUID:$PGID" "$POSTGRES_DIR" 2>/dev/null; then
|
||||
echo ""
|
||||
echo "================================================================"
|
||||
echo "ERROR: Cannot update ownership of $POSTGRES_DIR"
|
||||
echo " Current owner: UID $CURRENT_OWNER"
|
||||
echo " Target owner: UID $PUID (GID $PGID)"
|
||||
echo ""
|
||||
echo " This typically occurs with rootless Docker or restricted"
|
||||
echo " filesystems (NFS with root_squash, CIFS/SMB)."
|
||||
echo ""
|
||||
echo " To fix:"
|
||||
echo " - Local/NFS: sudo chown -R $PUID:$PGID <host_path_to_data>/db"
|
||||
echo " - CIFS/SMB: set the mount uid=$PUID,gid=$PGID option instead"
|
||||
echo " Then restart the container."
|
||||
echo "================================================================"
|
||||
echo ""
|
||||
exit 1
|
||||
fi
|
||||
chmod 700 "$POSTGRES_DIR"
|
||||
# Write sentinel LAST — if chown was interrupted, the sentinel
|
||||
# won't exist and next startup will re-run the full chown.
|
||||
write_ownership_sentinel
|
||||
echo "Ownership migration complete."
|
||||
fi
|
||||
|
||||
# --- 2. Authentication guarantee (unconditional) ---
|
||||
# Always rewrite pg_hba.conf to the known-good state. This replaces
|
||||
# any auth method (peer, ident, md5, scram) left by previous images
|
||||
# or initdb defaults. Eliminates the class of bugs where the OS user
|
||||
# name doesn't match any PG role under peer/ident auth.
|
||||
configure_pg_network "${POSTGRES_DIR}"
|
||||
fi
|
||||
|
||||
# Only run upgrade if current version is set and not the target
|
||||
if [ -n "$CURRENT_VERSION" ] && [ "$CURRENT_VERSION" != "$PG_VERSION" ]; then
|
||||
echo "Detected PostgreSQL data directory version $CURRENT_VERSION, upgrading to $PG_VERSION..."
|
||||
|
|
@ -56,6 +163,51 @@ if [[ "$DISPATCHARR_ENV" != "modular" ]]; then
|
|||
PG_INSTALLED_BY_SCRIPT=1
|
||||
fi
|
||||
|
||||
# Prepare the old cluster for pg_upgrade:
|
||||
# 1. Promote $POSTGRES_USER to superuser (needed for post-upgrade ops)
|
||||
# 2. Detect the bootstrap superuser (install user) — pg_upgrade
|
||||
# requires -U to match this role exactly.
|
||||
# The old cluster's install user is "postgres" (pre-PUID images)
|
||||
# or $POSTGRES_USER (post-PUID images, future upgrades).
|
||||
_pg_tmp_port=5499
|
||||
echo "Preparing old cluster for upgrade..."
|
||||
su - "$POSTGRES_USER" -c "$OLD_BINDIR/pg_ctl -D $POSTGRES_DIR start -w -o '-c port=$_pg_tmp_port'"
|
||||
_promoted=false
|
||||
for _role in "postgres" "$POSTGRES_USER"; do
|
||||
if su - "$POSTGRES_USER" -c "psql -U $_role -d template1 -p $_pg_tmp_port -tAc 'SELECT 1;'" 2>/dev/null | grep -q 1; then
|
||||
if su - "$POSTGRES_USER" -c "psql -U $_role -d template1 -p $_pg_tmp_port -v ON_ERROR_STOP=1" <<UPGEOF
|
||||
DO \$\$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = '$POSTGRES_USER') THEN
|
||||
CREATE ROLE $POSTGRES_USER WITH SUPERUSER LOGIN;
|
||||
ELSE
|
||||
ALTER ROLE $POSTGRES_USER WITH SUPERUSER;
|
||||
END IF;
|
||||
END
|
||||
\$\$;
|
||||
UPGEOF
|
||||
then
|
||||
_promoted=true
|
||||
break
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
# Detect the bootstrap superuser (OID 10 = the role that ran initdb).
|
||||
_install_user=$(su - "$POSTGRES_USER" -c "psql -d template1 -p $_pg_tmp_port -tAc \
|
||||
\"SELECT rolname FROM pg_authid WHERE oid = 10;\"" 2>/dev/null | tr -d '[:space:]')
|
||||
if [ -z "$_install_user" ]; then
|
||||
_install_user="postgres"
|
||||
fi
|
||||
|
||||
su - "$POSTGRES_USER" -c "$OLD_BINDIR/pg_ctl -D $POSTGRES_DIR stop -w"
|
||||
if [ "$_promoted" != true ]; then
|
||||
echo "❌ Failed to prepare old cluster for upgrade."
|
||||
echo " Could not promote '$POSTGRES_USER' to superuser in PG $CURRENT_VERSION."
|
||||
exit 1
|
||||
fi
|
||||
echo "Old cluster install user: $_install_user"
|
||||
|
||||
# Prepare new data directory
|
||||
NEW_POSTGRES_DIR="${POSTGRES_DIR}_$PG_VERSION"
|
||||
|
||||
|
|
@ -66,20 +218,26 @@ if [[ "$DISPATCHARR_ENV" != "modular" ]]; then
|
|||
fi
|
||||
|
||||
mkdir -p "$NEW_POSTGRES_DIR"
|
||||
chown -R postgres:postgres "$NEW_POSTGRES_DIR"
|
||||
chown -R $PUID:$PGID "$NEW_POSTGRES_DIR"
|
||||
chmod 700 "$NEW_POSTGRES_DIR"
|
||||
|
||||
# Initialize new data directory
|
||||
# Initialize new data directory with the same install user as the old
|
||||
# cluster. pg_upgrade requires the -U user to match both clusters.
|
||||
echo "Initializing new PostgreSQL data directory at $NEW_POSTGRES_DIR..."
|
||||
su - postgres -c "$NEW_BINDIR/initdb -D $NEW_POSTGRES_DIR"
|
||||
su - "$POSTGRES_USER" -c "$NEW_BINDIR/initdb -U $_install_user -D $NEW_POSTGRES_DIR"
|
||||
echo "Running pg_upgrade from $OLD_BINDIR to $NEW_BINDIR..."
|
||||
# Run pg_upgrade
|
||||
su - postgres -c "$NEW_BINDIR/pg_upgrade -b $OLD_BINDIR -B $NEW_BINDIR -d $POSTGRES_DIR -D $NEW_POSTGRES_DIR"
|
||||
su - "$POSTGRES_USER" -c "$NEW_BINDIR/pg_upgrade -U $_install_user -b $OLD_BINDIR -B $NEW_BINDIR -d $POSTGRES_DIR -D $NEW_POSTGRES_DIR"
|
||||
|
||||
# Move old data directory for backup, move new into place
|
||||
mv "$POSTGRES_DIR" "${POSTGRES_DIR}_backup_${CURRENT_VERSION}_$(date +%s)"
|
||||
mv "$NEW_POSTGRES_DIR" "$POSTGRES_DIR"
|
||||
|
||||
# Apply standard connection configuration to the upgraded data directory.
|
||||
configure_pg_network "${POSTGRES_DIR}"
|
||||
|
||||
# Record ownership sentinel for the newly upgraded data directory.
|
||||
write_ownership_sentinel
|
||||
|
||||
echo "Upgrade complete. Old data directory backed up."
|
||||
|
||||
# Uninstall PostgreSQL if we installed it just for upgrade
|
||||
|
|
@ -90,61 +248,154 @@ if [[ "$DISPATCHARR_ENV" != "modular" ]]; then
|
|||
fi
|
||||
fi
|
||||
|
||||
# Initialize PostgreSQL database
|
||||
if [ -z "$(ls -A $POSTGRES_DIR)" ]; then
|
||||
# Initialize PostgreSQL data directory (fresh install only).
|
||||
# Only runs initdb + configure_pg_network here. Database creation,
|
||||
# role setup, and password configuration are handled by the
|
||||
# unconditional guarantees (promote_app_role, ensure_app_database)
|
||||
# after PostgreSQL starts in entrypoint.sh.
|
||||
if [ -z "$(ls -A "$POSTGRES_DIR")" ]; then
|
||||
echo "Initializing PostgreSQL database..."
|
||||
mkdir -p $POSTGRES_DIR
|
||||
chown -R postgres:postgres $POSTGRES_DIR
|
||||
chmod 700 $POSTGRES_DIR
|
||||
mkdir -p "$POSTGRES_DIR"
|
||||
chown -R "$PUID:$PGID" "$POSTGRES_DIR"
|
||||
chmod 700 "$POSTGRES_DIR"
|
||||
|
||||
# Initialize PostgreSQL
|
||||
su - postgres -c "$PG_BINDIR/initdb -D ${POSTGRES_DIR}"
|
||||
# Configure PostgreSQL
|
||||
echo "host all all 0.0.0.0/0 md5" >> "${POSTGRES_DIR}/pg_hba.conf"
|
||||
echo "listen_addresses='*'" >> "${POSTGRES_DIR}/postgresql.conf"
|
||||
# Initialize PostgreSQL as the application user.
|
||||
# The superuser role is automatically named $POSTGRES_USER.
|
||||
su - "$POSTGRES_USER" -c "$PG_BINDIR/initdb -D ${POSTGRES_DIR}"
|
||||
|
||||
# Start PostgreSQL
|
||||
echo "Starting Postgres..."
|
||||
su - postgres -c "$PG_BINDIR/pg_ctl -D ${POSTGRES_DIR} start -w -t 300 -o '-c port=${POSTGRES_PORT}'"
|
||||
# Wait for PostgreSQL to be ready
|
||||
until su - postgres -c "$PG_BINDIR/pg_isready -h ${POSTGRES_HOST} -p ${POSTGRES_PORT}" >/dev/null 2>&1; do
|
||||
echo "Waiting for PostgreSQL to be ready..."
|
||||
sleep 1
|
||||
done
|
||||
# Configure authentication and network access.
|
||||
configure_pg_network "${POSTGRES_DIR}"
|
||||
|
||||
postgres_pid=$(su - postgres -c "$PG_BINDIR/pg_ctl -D ${POSTGRES_DIR} status" | sed -n 's/.*PID: \([0-9]\+\).*/\1/p')
|
||||
|
||||
# Setup database if needed
|
||||
if ! su - postgres -c "psql -p ${POSTGRES_PORT} -tAc \"SELECT 1 FROM pg_database WHERE datname = '$POSTGRES_DB';\"" | grep -q 1; then
|
||||
# Create PostgreSQL database
|
||||
echo "Creating PostgreSQL database..."
|
||||
su - postgres -c "createdb -p ${POSTGRES_PORT} --encoding=UTF8 ${POSTGRES_DB}"
|
||||
# Create user, set ownership, and grant privileges
|
||||
echo "Creating PostgreSQL user..."
|
||||
su - postgres -c "psql -p ${POSTGRES_PORT} -d ${POSTGRES_DB}" <<EOF
|
||||
DO \$\$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = '$POSTGRES_USER') THEN
|
||||
CREATE ROLE $POSTGRES_USER WITH LOGIN PASSWORD '$POSTGRES_PASSWORD';
|
||||
END IF;
|
||||
END
|
||||
\$\$;
|
||||
EOF
|
||||
echo "Setting PostgreSQL user privileges..."
|
||||
su postgres -c "$PG_BINDIR/psql -p ${POSTGRES_PORT} -c \"ALTER DATABASE ${POSTGRES_DB} OWNER TO $POSTGRES_USER;\""
|
||||
su postgres -c "$PG_BINDIR/psql -p ${POSTGRES_PORT} -c \"GRANT ALL PRIVILEGES ON DATABASE ${POSTGRES_DB} TO $POSTGRES_USER;\""
|
||||
# Finished setting up PosgresSQL database
|
||||
echo "PostgreSQL database setup complete."
|
||||
fi
|
||||
|
||||
kill $postgres_pid
|
||||
while kill -0 $postgres_pid; do
|
||||
sleep 1
|
||||
done
|
||||
# Record ownership sentinel for the freshly initialized data directory.
|
||||
write_ownership_sentinel
|
||||
fi
|
||||
|
||||
fi # End of DISPATCHARR_ENV != modular check
|
||||
|
||||
# =========================================================================
|
||||
# 3. Role guarantee (unconditional — runs after PostgreSQL starts)
|
||||
#
|
||||
# Ensures the application role ($POSTGRES_USER) exists with superuser
|
||||
# privileges and the correct password. Called from entrypoint.sh after
|
||||
# PostgreSQL starts on every AIO startup.
|
||||
#
|
||||
# Idempotent: checks before altering. Handles all scenarios:
|
||||
# - Fresh install: role exists from initdb, just verifies
|
||||
# - Upgrade from postgres-user: creates dispatch role, promotes to superuser
|
||||
# - PUID change: verifies existing role, updates password
|
||||
# - Normal restart: no-op (role already correct)
|
||||
#
|
||||
# Tries multiple database/role combinations to handle incomplete data
|
||||
# (e.g., interrupted initialization from a previous image version).
|
||||
# =========================================================================
|
||||
promote_app_role() {
|
||||
if [[ "$DISPATCHARR_ENV" == "modular" ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo "Ensuring application role is configured..."
|
||||
|
||||
# Find a connectable superuser role. Try multiple databases in case
|
||||
# the default 'postgres' database doesn't exist (e.g., incomplete
|
||||
# initialization from a crashed previous container).
|
||||
# Single query per candidate: if connection fails, output is empty;
|
||||
# if connected but not superuser, output is 'f'. Only 't' passes.
|
||||
CONNECT_ROLE=""
|
||||
CONNECT_DB=""
|
||||
for try_db in "postgres" "template1"; do
|
||||
for try_role in "postgres" "$POSTGRES_USER"; do
|
||||
local _super
|
||||
_super=$(su - $POSTGRES_USER -c "psql -U $try_role -d $try_db -p ${POSTGRES_PORT} -tAc \
|
||||
\"SELECT rolsuper FROM pg_roles WHERE rolname='$try_role';\"" 2>/dev/null | tr -d '[:space:]')
|
||||
if [ "$_super" = "t" ]; then
|
||||
CONNECT_ROLE="$try_role"
|
||||
CONNECT_DB="$try_db"
|
||||
break 2
|
||||
fi
|
||||
done
|
||||
done
|
||||
|
||||
if [ -z "$CONNECT_ROLE" ]; then
|
||||
echo "❌ Role setup failed: no connectable superuser role found."
|
||||
echo " To recover manually:"
|
||||
echo " su - $POSTGRES_USER -c \"psql -d template1 -p $POSTGRES_PORT\""
|
||||
echo " CREATE ROLE $POSTGRES_USER WITH SUPERUSER LOGIN PASSWORD '<your_password>';"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Escape single quotes for safe SQL interpolation
|
||||
local _sql_pw="${POSTGRES_PASSWORD//\'/\'\'}"
|
||||
|
||||
if ! su - $POSTGRES_USER -c "psql -U $CONNECT_ROLE -d $CONNECT_DB -p ${POSTGRES_PORT} -v ON_ERROR_STOP=1" <<EOSQL
|
||||
DO \$\$
|
||||
BEGIN
|
||||
-- Ensure the application role exists with superuser and login.
|
||||
IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = '$POSTGRES_USER') THEN
|
||||
CREATE ROLE $POSTGRES_USER WITH SUPERUSER LOGIN PASSWORD '${_sql_pw}';
|
||||
ELSE
|
||||
-- Only alter if not already superuser (idempotent).
|
||||
IF NOT (SELECT rolsuper FROM pg_roles WHERE rolname = '$POSTGRES_USER') THEN
|
||||
ALTER ROLE $POSTGRES_USER WITH SUPERUSER LOGIN;
|
||||
END IF;
|
||||
-- Ensure password is current regardless.
|
||||
ALTER ROLE $POSTGRES_USER WITH PASSWORD '${_sql_pw}';
|
||||
END IF;
|
||||
|
||||
-- Rollback compatibility: preserve the postgres role as superuser so
|
||||
-- older images (which connect as the postgres DB role) continue to work.
|
||||
-- This block can be removed once rollback to pre-PUID images is no
|
||||
-- longer expected.
|
||||
IF EXISTS (SELECT FROM pg_roles WHERE rolname = 'postgres') THEN
|
||||
IF NOT (SELECT rolsuper FROM pg_roles WHERE rolname = 'postgres') THEN
|
||||
ALTER ROLE postgres WITH SUPERUSER;
|
||||
END IF;
|
||||
END IF;
|
||||
END
|
||||
\$\$;
|
||||
EOSQL
|
||||
then
|
||||
echo "❌ Role setup failed. The application may not be able to connect."
|
||||
echo " Check PostgreSQL logs for details."
|
||||
echo " To recover manually:"
|
||||
echo " su - $POSTGRES_USER -c \"psql -d template1 -p $POSTGRES_PORT\""
|
||||
echo " ALTER ROLE $POSTGRES_USER WITH SUPERUSER LOGIN PASSWORD '<your_password>';"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✅ Application role configured."
|
||||
}
|
||||
|
||||
# =========================================================================
|
||||
# 4. Database guarantee (unconditional — runs after role setup)
|
||||
#
|
||||
# Ensures the application database ($POSTGRES_DB) exists. Handles
|
||||
# incomplete data from interrupted previous initializations where
|
||||
# PG_VERSION exists but the application database was never created.
|
||||
# =========================================================================
|
||||
ensure_app_database() {
|
||||
if [[ "$DISPATCHARR_ENV" == "modular" ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Connect to template1 (always exists) to check pg_database catalog.
|
||||
if su - $POSTGRES_USER -c "psql -d template1 -p ${POSTGRES_PORT} -tAc \
|
||||
\"SELECT 1 FROM pg_database WHERE datname = '$POSTGRES_DB';\"" 2>/dev/null | grep -q 1; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo "Application database '$POSTGRES_DB' not found — creating..."
|
||||
if ! su - $POSTGRES_USER -c "createdb -p ${POSTGRES_PORT} --encoding=UTF8 ${POSTGRES_DB}" 2>/dev/null; then
|
||||
# Might already exist if the check failed for a transient reason.
|
||||
if su - $POSTGRES_USER -c "psql -d template1 -p ${POSTGRES_PORT} -tAc \
|
||||
\"SELECT 1 FROM pg_database WHERE datname = '$POSTGRES_DB';\"" 2>/dev/null | grep -q 1; then
|
||||
return 0
|
||||
fi
|
||||
echo "❌ Failed to create database '$POSTGRES_DB'"
|
||||
exit 1
|
||||
fi
|
||||
echo "✅ Database '$POSTGRES_DB' created."
|
||||
}
|
||||
|
||||
ensure_utf8_encoding() {
|
||||
# Check encoding of existing database
|
||||
# Supports both internal (Unix socket) and external (TCP) PostgreSQL
|
||||
|
|
@ -154,8 +405,8 @@ ensure_utf8_encoding() {
|
|||
# External database: use TCP connection with password
|
||||
CURRENT_ENCODING=$(PGPASSWORD="$POSTGRES_PASSWORD" psql -w -h "$POSTGRES_HOST" -p "$POSTGRES_PORT" -U "$POSTGRES_USER" -d "$POSTGRES_DB" -tAc "SELECT pg_encoding_to_char(encoding) FROM pg_database WHERE datname = current_database();" 2>/dev/null | tr -d ' ')
|
||||
else
|
||||
# Internal database: use Unix socket as postgres user
|
||||
CURRENT_ENCODING=$(su - postgres -c "psql -p ${POSTGRES_PORT} -d ${POSTGRES_DB} -tAc \"SELECT pg_encoding_to_char(encoding) FROM pg_database WHERE datname = current_database();\"" | tr -d ' ')
|
||||
# Internal database: use Unix socket as application user
|
||||
CURRENT_ENCODING=$(su - $POSTGRES_USER -c "psql -p ${POSTGRES_PORT} -d ${POSTGRES_DB} -tAc \"SELECT pg_encoding_to_char(encoding) FROM pg_database WHERE datname = current_database();\"" | tr -d ' ')
|
||||
fi
|
||||
|
||||
if [ "$CURRENT_ENCODING" != "UTF8" ]; then
|
||||
|
|
@ -173,15 +424,15 @@ ensure_utf8_encoding() {
|
|||
# Restore data
|
||||
PGPASSWORD="$POSTGRES_PASSWORD" psql -w -h "$POSTGRES_HOST" -p "$POSTGRES_PORT" -U "$POSTGRES_USER" -d "$POSTGRES_DB" < "$DUMP_FILE" || { echo "Restore failed"; return 1; }
|
||||
else
|
||||
# Internal database: use Unix socket as postgres user
|
||||
# Internal database: use Unix socket as application user
|
||||
# Dump database (include permissions and ownership)
|
||||
su - postgres -c "pg_dump -p ${POSTGRES_PORT} ${POSTGRES_DB}" > "$DUMP_FILE" || { echo "Dump failed"; return 1; }
|
||||
su - $POSTGRES_USER -c "pg_dump -p ${POSTGRES_PORT} ${POSTGRES_DB}" > "$DUMP_FILE" || { echo "Dump failed"; return 1; }
|
||||
# Drop and recreate database with UTF8 encoding using template0
|
||||
su - postgres -c "dropdb -p ${POSTGRES_PORT} ${POSTGRES_DB}" || { echo "Drop failed"; return 1; }
|
||||
su - $POSTGRES_USER -c "dropdb -p ${POSTGRES_PORT} ${POSTGRES_DB}" || { echo "Drop failed"; return 1; }
|
||||
# Recreate database with UTF8 encoding and correct owner
|
||||
su - postgres -c "createdb -p ${POSTGRES_PORT} --encoding=UTF8 --template=template0 --owner=${POSTGRES_USER} ${POSTGRES_DB}" || { echo "Create failed"; return 1; }
|
||||
su - $POSTGRES_USER -c "createdb -p ${POSTGRES_PORT} --encoding=UTF8 --template=template0 --owner=${POSTGRES_USER} ${POSTGRES_DB}" || { echo "Create failed"; return 1; }
|
||||
# Restore data
|
||||
cat "$DUMP_FILE" | su - postgres -c "psql -p ${POSTGRES_PORT} -d ${POSTGRES_DB}" || { echo "Restore failed"; return 1; }
|
||||
cat "$DUMP_FILE" | su - $POSTGRES_USER -c "psql -p ${POSTGRES_PORT} -d ${POSTGRES_DB}" || { echo "Restore failed"; return 1; }
|
||||
fi
|
||||
|
||||
rm -f "$DUMP_FILE"
|
||||
|
|
@ -249,5 +500,3 @@ check_external_postgres_version() {
|
|||
|
||||
return 0
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -65,11 +65,9 @@ if [ "$(id -u)" = "0" ]; then
|
|||
fi
|
||||
done
|
||||
|
||||
# Database permissions
|
||||
if [ -d /data/db ] && [ "$(stat -c '%u' /data/db)" != "$(id -u postgres)" ]; then
|
||||
echo "Fixing ownership for /data/db"
|
||||
chown -R postgres:postgres /data/db
|
||||
fi
|
||||
# /data/db ownership is handled by 02-postgres.sh (sentinel-based reconciliation).
|
||||
# No secondary check needed here — duplicating it could chown without updating
|
||||
# the sentinel, creating inconsistent state.
|
||||
|
||||
# Fix /data directory ownership (non-recursive)
|
||||
if [ -d "/data" ] && [ "$(stat -c '%u:%g' /data)" != "$PUID:$PGID" ]; then
|
||||
|
|
|
|||
1517
docker/tests/test-puid-pgid.sh
Normal file
1517
docker/tests/test-puid-pgid.sh
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue