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:
None 2026-03-11 23:35:38 -05:00
parent 0bc4c40400
commit 52ed0fc15a
5 changed files with 1892 additions and 73 deletions

View file

@ -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)

View file

@ -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)

View file

@ -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
}

View file

@ -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

File diff suppressed because it is too large Load diff