Project

General

Profile

fwd_create_pg_cluster.sh

Roger Borrello, 09/09/2025 06:10 PM

Download (10.8 KB)

 
1
#!/usr/bin/env bash
2
set -euo pipefail
3

    
4
# set -x
5

    
6
# Defaults
7
PGDATA_DEFAULT="/var/lib/postgresql/data"
8
PGPORT_DEFAULT=5342
9
LOCALE_DEFAULT="en_US.UTF-8"
10
ENCODING_DEFAULT="UTF8"
11
AUTH_DEFAULT="scram-sha-256"   # alternatives: md5 | password | trust
12
TEMP_PORT_DEFAULT="55432"
13
SOCKET_DIR_DEFAULT="/tmp"
14
ROLEFILE_PATH_DEFAULT="."
15

    
16
# Args
17
PGDATA="$PGDATA_DEFAULT"
18
PGPORT="$PGPORT_DEFAULT"
19
DB_LOCALE="$LOCALE_DEFAULT"
20
DB_ENCODING="$ENCODING_DEFAULT"
21
AUTH_METHOD="$AUTH_DEFAULT"
22
TEMP_PORT="$TEMP_PORT_DEFAULT"
23
SOCKET_DIR="$SOCKET_DIR_DEFAULT"
24
ROLEFILE_PATH="$ROLEFILE_PATH_DEFAULT"
25

    
26
POSTGRES_PASSWORD=""
27
ADMIN_USER="fwd_admin"
28
ADMIN_PASS=""
29
STANDARD_USER="fwd_user"
30
STANDARD_PASS=""
31
force=false
32
start_cluster=false
33

    
34
function show_usage()
35
{
36
   cat <<EOF
37
Usage: $0 [options]
38

    
39
Creates a PostgreSQL cluster with upstream initdb, compatible with the official
40
postgres Docker image. Safe to run on Ubuntu (host) or in an Ubuntu-based container.
41

    
42
Options:
43
  --pgdata DIR                 Data directory (default: $PGDATA_DEFAULT)
44
  --pgport PORT                Conection port (default: $PGPORT_DEFAULT)
45
  --locale LOCALE              Cluster locale (default: $LOCALE_DEFAULT)
46
  --encoding ENC               Cluster encoding (default: $ENCODING_DEFAULT)
47
  --auth METHOD                pg_hba auth method: scram-sha-256 | md5 | password | trust
48
                               (default: $AUTH_DEFAULT)
49

    
50
  --postgres-password PASS     Initial password for postgres superuser
51
  --admin-user NAME            Admin role name (default: $ADMIN_USER)
52
  --admin-pass PASS            Admin role password
53
  --user-user NAME             Standard role name (default: $STANDARD_USER)
54
  --user-pass PASS             Standard role password
55

    
56
  --temp-port PORT             Temporary port for bootstrap (default: $TEMP_PORT_DEFAULT)
57
  --socket-dir DIR             Unix socket dir for bootstrap (default: $SOCKET_DIR_DEFAULT)
58
  --rolefile-path FILE         Path to create_roles.sql file (default: $ROLEFILE_PATH_DEFAULT)
59

    
60
  --force                      Remove the cluster if it already exists.
61
  --start                      Start the cluster after creating it.
62

    
63
Examples:
64
  $0 --pgdata /data/pg17 --locale en_US.UTF-8 --encoding UTF8 \\
65
     --pgport 5433
66
     --auth scram-sha-256 \\
67
     --postgres-password adminpw \\
68
     --admin-user app_admin --admin-pass S3cret! \\
69
     --user-user app_user --user-pass userpw
70

    
71
Notes:
72
- Uses initdb (NOT pg_createcluster), so the layout matches postgres Docker images.
73
- Ensures correct ownership and permissions on PGDATA.
74
EOF
75
}
76

    
77
# Parse args
78
while [[ $# -gt 0 ]]; do
79
   case "$1" in
80
      --pgdata)             PGDATA="$2"; shift 2 ;;
81
      --pgport)             PGPORT="$2"; shift 2 ;;
82
      --locale)             DB_LOCALE="$2"; shift 2 ;;
83
      --encoding)           DB_ENCODING="$2"; shift 2 ;;
84
      --auth)               AUTH_METHOD="$2"; shift 2 ;;
85
      --postgres-password)  POSTGRES_PASSWORD="$2"; shift 2 ;;
86
      --admin-user)         ADMIN_USER="$2"; shift 2 ;;
87
      --admin-pass)         ADMIN_PASS="$2"; shift 2 ;;
88
      --user-user)          STANDARD_USER="$2"; shift 2 ;;
89
      --user-pass)          STANDARD_PASS="$2"; shift 2 ;;
90
      --temp-port)          TEMP_PORT="$2"; shift 2 ;;
91
      --socket-dir)         SOCKET_DIR="$2"; shift 2 ;;
92
      --rolefile-path)      ROLEFILE_PATH="$2"; shift 2 ;;
93
      --force)              force=true; shift 1 ;;
94
      --start)              start_cluster=true; shift 1 ;;
95
      -h|--help|-?)         show_usage; exit 0 ;;
96
      *) echo "ERROR: Unknown option: $1"; show_usage; exit 1 ;;
97
   esac
98
done
99

    
100
function force_drop()
101
{
102
   echo "INFO: Checking if cluster is running..."
103
   if ${pg_path}/pg_ctl -D "$PGDATA" status >/dev/null 2>&1; then
104
     echo "INFO: Stopping cluster..."
105
     run_as_postgres ${pg_path}/pg_ctl -D "$PGDATA" stop -m fast
106
   else
107
     echo "INFO: Cluster not running."
108
   fi
109

    
110
   # Delete data directory
111
   if [[ -d "$PGDATA" ]]; then
112
      echo "INFO: Removing data directory $PGDATA"
113
      sudo rm -rf "$PGDATA"
114
   fi
115
}
116

    
117
# ---- Helper: run command as the postgres system user when available ----
118
function run_as_postgres()
119
{
120
   if command -v gosu >/dev/null && [ "$(id -u)" = "0" ]; then
121
      gosu postgres "$@"
122
   elif [ "$(id -un)" = "postgres" ]; then
123
      "$@"
124
   else
125
      sudo -u postgres "$@"
126
   fi
127
}
128

    
129
# ---- Checks ----
130
function need()
131
{
132
   command -v "$1" >/dev/null 2>&1 || { echo "ERROR: Missing required command: $1" >&2; exit 1; }
133
}
134

    
135
# Setup full path, since sudo gets a PATH without PG
136
pg_path="/usr/lib/postgresql/${PG_VERSION}/bin"
137
need ${pg_path}/initdb
138
need ${pg_path}/pg_ctl
139
need ${pg_path}/psql
140

    
141
# ---- Prepare PGDATA (ownership and permissions) ----
142
PGDATA="$(readlink -f "$PGDATA")"
143
CLUSTER_NAME="${PGDATA##*/}"
144
PG_BASE="${PGDATA%/*}"
145

    
146
# ---- Check if cluster exists. ----
147
if run_as_postgres test -s "$PGDATA/PG_VERSION"; then
148
   if [ "$force" == true ]; then
149
      echo "WARNING: Cluster $CLUSTER_NAME already exists at $PGDATA, but --force specified. Dropping cluster."
150
      force_drop
151
   else
152
      echo "WARNING: Cluster $CLUSTER_NAME already exists at $PGDATA; exiting."
153
      exit 1
154
   fi
155
fi
156

    
157
sudo mkdir -p "$PGDATA"
158
# Ensure ownership by postgres user (UID/GID may be 999:999 inside official postgres image)
159
if id postgres >/dev/null 2>&1; then
160
   sudo chown -R postgres:postgres "$PGDATA" || true
161
fi
162
sudo chmod 700 "$PGDATA" || true
163

    
164
echo "INFO: Initializing cluster in $PGDATA (locale=$DB_LOCALE, encoding=$DB_ENCODING)..."
165

    
166
PWFILE=""
167
if [[ -n "$POSTGRES_PASSWORD" ]]; then
168
   PWFILE="$(mktemp)"
169
   printf '%s' "$POSTGRES_PASSWORD" > "$PWFILE"
170
   sudo chown postgres:postgres "$PWFILE"
171
fi
172

    
173
initdb_args=( -D "$PGDATA" --locale="$DB_LOCALE" --encoding="$DB_ENCODING" )
174
# Set bootstrap superuser name explicitly (default is 'postgres' anyway)
175
initdb_args+=( -U postgres )
176
if [[ -n "$PWFILE" ]]; then
177
   initdb_args+=( --pwfile="$PWFILE" )
178
fi
179

    
180
run_as_postgres ${pg_path}/initdb "${initdb_args[@]}"
181

    
182
[[ -n "$PWFILE" ]] && run_as_postgres rm -f "$PWFILE"
183

    
184
# ---- Configure password_encryption if SCRAM selected ----
185
POSTGRESQL_CONF="${PGDATA}/postgresql.conf"
186
echo "INFO: Configuring $POSTGRESQL_CONF with auth method: $AUTH_METHOD"
187
if [[ "$AUTH_METHOD" == "scram-sha-256" ]]; then
188
   #if ! grep -q '^[# ]*password_encryption[[:space:]]*=' "$POSTGRESQL_CONF" 2>/dev/null; then
189
   #   echo "password_encryption = scram-sha-256" | run_as_postgres tee -a "$POSTGRESQL_CONF" > /dev/null
190
   #else
191
      #run_as_postgres sed -i -e "s|^[# ]*password_encryption[[:space:]]*=.*|password_encryption = scram-sha-256|g" "$POSTGRESQL_CONF"
192
      run_as_postgres sed -i -e 's/^\s*#\s*\(password_encryption\s*=\s*\).*$/\1scram-sha-256/g' "$POSTGRESQL_CONF"
193
   #fi
194
fi
195
run_as_postgres sed -i -e 's/^\s*#\(\s*log_destination\)\( *=\)\(.*\)$/\1\2\3/g' \
196
                       -e 's/^\s*#\(\s*logging_collector\)\( *=\)\(.*\)$/\1\2\3/g' \
197
                       -e 's/^\s*#\(\s*log_directory\)\( *=\)\(.*\)$/\1\2\3/g' \
198
                       -e 's/^\s*#\(\s*log_filename\)\( *=\)\(.*\)$/\1\2\3/g' \
199
                       -e 's/^\s*#\(\s*log_file_mode\)\( *=\)\(.*\)$/\1\2\3/g' \
200
                       -e 's/^\s*#\(\s*log_rotation_age\)\( *=\)\(.*\)$/\1\2\3/g' \
201
                       -e 's/^\s*#\(\s*log_rotation_size\)\( *=\)\(.*\)$/\1\2\3/g' \
202
                       -e "s/^\s*#\(\s*port\)\( *=\)\(.*\)$/\1\2 ${PGPORT}/g" \
203
                "$POSTGRESQL_CONF"
204

    
205
# Ensure server will accept TCP later (listen on all; comment/uncomment as needed)
206
run_as_postgres sed -i -e 's/^\s*#\s*\(listen_addresses\s*=\s*\).*$/\1'\''*'\''/g' "$POSTGRESQL_CONF"
207

    
208
# ---- Configure pg_hba.conf ----
209
PG_HBA="${PGDATA}/pg_hba.conf"
210
echo "INFO: Configuring $PG_HBA with auth method: $AUTH_METHOD"
211
run_as_postgres sed -i -e "s/\(^local\s\+all\s\+all\s\+\)trust$/\1${AUTH_METHOD}/g" \
212
                       -e "s/\(^host\s\+all\s\+all\s\+127\.0\.0\.1\/32\s\+\)trust$/\1${AUTH_METHOD}/g" \
213
                       -e "s/\(^host\s\+all\s\+all\s\+\:\:1\/128\s\+\)trust$/\1${AUTH_METHOD}/g" \
214
                       -e "s/\(^local\s\+replication\s\+all\s\+\)trust$/\1${AUTH_METHOD}/g" \
215
                       -e "s/\(^host\s\+replication\s\+all\s\+127\.0\.0\.1\/32\s\+\)trust$/\1${AUTH_METHOD}/g" \
216
                       -e "s/\(^host\s\+replication\s\+all\s\+\:\:1\/128\s\+\)trust$/\1${AUTH_METHOD}/g" \
217
                   "$PG_HBA"
218
# Keep any existing lines, but ensure we have our standard baseline at the end
219
echo "host all all all $AUTH_METHOD" | run_as_postgres tee -a "$PG_HBA" > /dev/null
220

    
221
# Create a temporary pg_hba.conf with trust authentication
222
BOOTSTRAP_HBA="${PGDATA}/pg_hba.bootstrap.conf"
223
sudo tee "$BOOTSTRAP_HBA" > /dev/null <<'HBA'
224
# Bootstrap-only HBA (trust all local connections)
225
local   all   all                 trust
226
host    all   all   127.0.0.1/32  trust
227
host    all   all   ::1/128       trust
228
HBA
229
sudo chown -R postgres:postgres "$BOOTSTRAP_HBA"
230
sudo chmod 600 "$BOOTSTRAP_HBA"
231
sudo cp "${ROLEFILE_PATH}/create_roles.sql" /tmp
232
sudo chown postgres:postgres /tmp/create_roles.sql
233

    
234
# ---- Bootstrap with HBA locally (socket only) to create roles ----
235
echo "INFO: Starting temporary server for role creation..."
236
run_as_postgres ${pg_path}/pg_ctl -D "$PGDATA" \
237
  -o "-c listen_addresses='' -c unix_socket_directories='$SOCKET_DIR' -p $TEMP_PORT -c hba_file=$BOOTSTRAP_HBA" \
238
  -w start
239

    
240
# Setup 'set' variables
241
PSQL_ARGS=(
242
  --set=std_user="${STANDARD_USER}"
243
  --set=std_pass="${STANDARD_PASS:-}"
244
  --set=admin_user="${ADMIN_USER}"
245
  --set=admin_pass="${ADMIN_PASS:-}"
246
)
247
[[ -n "$POSTGRES_PASSWORD" ]] && PSQL_ARGS+=(--set=pg_pass="$POSTGRES_PASSWORD")
248

    
249
# Run external SQL file
250
echo "INFO: Creating roles..."
251
run_as_postgres psql -h "$SOCKET_DIR" -p "$TEMP_PORT" -U postgres -d postgres \
252
                     "${PSQL_ARGS[@]}" \
253
                     -v ON_ERROR_STOP=1 \
254
                     -f "/tmp/create_roles.sql"
255

    
256
# Stop temp server
257
echo "INFO: Stopping temporary server..."
258
run_as_postgres ${pg_path}/pg_ctl -D "$PGDATA" -w stop
259

    
260
# Final ownership/permissions setup
261
if id postgres >/dev/null 2>&1; then
262
   sudo chown -R postgres:postgres "$PGDATA" || true
263
fi
264
sudo chmod 700 "$PGDATA" || true
265

    
266
# Cleanup bootstrap hba and roles file
267
sudo rm -f "$BOOTSTRAP_HBA" || true
268
sudo rm -f "/tmp/create_roles.sql" || true
269

    
270
# Start the cluster, if requested
271
[ "$start_cluster" == true ] && run_as_postgres ${pg_path}/pg_ctl -D ${PGDATA} -w start || true
272

    
273
if ${pg_path}/pg_ctl -D "$PGDATA" status >/dev/null 2>&1; then
274
   echo ✅ Cluster is running at: $PGDATA
275
else
276
# Show some useful information
277
   cat <<SUM
278

    
279
✅ Cluster is ready to run at: $PGDATA
280

    
281
Next steps:
282
- To run with the official postgres image:
283
    docker run -e POSTGRES_PASSWORD='${POSTGRES_PASSWORD:-<set>}' \\
284
               -v <volume_name>:"$PGDATA" \\
285
               -p $PGPORT:$PGPORT \\
286
               postgres:${PG_VERSION}
287

    
288
- Or start locally (outside Docker):
289
    as postgres:  ${pg_path}/pg_ctl -D "$PGDATA" -w start
290

    
291
Notes:
292
- listen_addresses='*' has been set. Adjust pg_hba.conf to restrict host access before exposing ports.
293
- Auth method in pg_hba.conf: $AUTH_METHOD
294
SUM
295
fi