Project

General

Profile

fwd_create_pg_cluster.sh

Roger Borrello, 11/24/2025 09:33 AM

Download (11.4 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=5432
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
MAX_CONNECTIONS_DEFAULT=100
16

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

    
28
POSTGRES_PASSWORD=""
29
ADMIN_USER="fwd_admin"
30
ADMIN_PASS="admin"
31
STANDARD_USER="fwd_user"
32
STANDARD_PASS="user"
33
force=false
34
start_cluster=false
35

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

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

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

    
52
  --max-connections VALUE      Maximum connections (default: $MAX_CONNECTIONS_DEFAULT)
53

    
54
  --postgres-password PASS     Initial password for postgres superuser
55
  --admin-user NAME            Admin role name (default: $ADMIN_USER)
56
  --admin-pass PASS            Admin role password
57
  --user-user NAME             Standard role name (default: $STANDARD_USER)
58
  --user-pass PASS             Standard role password
59

    
60
  --temp-port PORT             Temporary port for bootstrap (default: $TEMP_PORT_DEFAULT)
61
  --socket-dir DIR             Unix socket dir for bootstrap (default: $SOCKET_DIR_DEFAULT)
62
  --rolefile-path FILE         Path to create_roles.sql file (default: $ROLEFILE_PATH_DEFAULT)
63

    
64
  --force                      Remove the cluster if it already exists.
65
  --start                      Start the cluster after creating it.
66

    
67
Examples:
68
  $0 --pgdata /data/pg17 --locale en_US.UTF-8 --encoding UTF8 \\
69
     --pgport 5433 \\
70
     --max-connections 200 \\
71
     --auth scram-sha-256 \\
72
     --postgres-password adminpw \\
73
     --admin-user app_admin --admin-pass S3cret! \\
74
     --user-user app_user --user-pass userpw
75

    
76
Notes:
77
- Uses initdb (NOT pg_createcluster), so the layout matches postgres Docker images.
78
- Ensures correct ownership and permissions on PGDATA.
79
EOF
80
}
81

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

    
106
function force_drop()
107
{
108
   echo "INFO: Checking if cluster is running..."
109
   if ${pg_path}/pg_ctl -D "$PGDATA" status >/dev/null 2>&1; then
110
     echo "INFO: Stopping cluster..."
111
     run_as_postgres ${pg_path}/pg_ctl -D "$PGDATA" stop -m fast
112
   else
113
     echo "INFO: Cluster not running."
114
   fi
115

    
116
   # Delete data directory
117
   if [[ -d "$PGDATA" ]]; then
118
      echo "INFO: Removing data directory $PGDATA"
119
      sudo rm -rf "$PGDATA"
120
   fi
121
}
122

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

    
135
# ---- Checks ----
136
function need()
137
{
138
   command -v "$1" >/dev/null 2>&1 || { echo "ERROR: Missing required command: $1" >&2; exit 1; }
139
}
140

    
141
# Setup full path, since sudo gets a PATH without PG
142
pg_path="/usr/lib/postgresql/${PG_VERSION}/bin"
143
need ${pg_path}/initdb
144
need ${pg_path}/pg_ctl
145
need ${pg_path}/psql
146

    
147
# ---- Prepare PGDATA (ownership and permissions) ----
148
PGDATA="$(readlink -f "$PGDATA")"
149
CLUSTER_NAME="${PGDATA##*/}"
150
PG_BASE="${PGDATA%/*}"
151

    
152
# ---- Check if cluster exists. ----
153
if run_as_postgres test -s "$PGDATA/PG_VERSION"; then
154
   if [ "$force" == true ]; then
155
      echo "WARNING: Cluster $CLUSTER_NAME already exists at $PGDATA, but --force specified. Dropping cluster."
156
      force_drop
157
   else
158
      echo "WARNING: Cluster $CLUSTER_NAME already exists at $PGDATA; exiting."
159
      exit 1
160
   fi
161
fi
162

    
163
sudo mkdir -p "$PGDATA"
164
# Ensure ownership by postgres user (UID/GID may be 999:999 inside official postgres image)
165
if id postgres >/dev/null 2>&1; then
166
   sudo chown -R postgres:postgres "$PGDATA" || true
167
fi
168
sudo chmod 700 "$PGDATA" || true
169

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

    
172
PWFILE=""
173
if [[ -n "$POSTGRES_PASSWORD" ]]; then
174
   PWFILE="$(mktemp)"
175
   printf '%s' "$POSTGRES_PASSWORD" > "$PWFILE"
176
   sudo chown postgres:postgres "$PWFILE"
177
fi
178

    
179
initdb_args=( -D "$PGDATA" --locale="$DB_LOCALE" --encoding="$DB_ENCODING" )
180
# Set bootstrap superuser name explicitly (default is 'postgres' anyway)
181
initdb_args+=( -U postgres )
182
if [[ -n "$PWFILE" ]]; then
183
   initdb_args+=( --pwfile="$PWFILE" )
184
fi
185

    
186
pushd /tmp >/dev/null
187
run_as_postgres ${pg_path}/initdb "${initdb_args[@]}"
188
popd >/dev/null
189

    
190
[[ -n "$PWFILE" ]] && run_as_postgres rm -f "$PWFILE"
191

    
192
# ---- Configure password_encryption if SCRAM selected ----
193
POSTGRESQL_CONF="${PGDATA}/postgresql.conf"
194
echo "INFO: Configuring $POSTGRESQL_CONF with auth method: $AUTH_METHOD"
195
if [[ "$AUTH_METHOD" == "scram-sha-256" ]]; then
196
   #if ! grep -q '^[# ]*password_encryption[[:space:]]*=' "$POSTGRESQL_CONF" 2>/dev/null; then
197
   #   echo "password_encryption = scram-sha-256" | run_as_postgres tee -a "$POSTGRESQL_CONF" > /dev/null
198
   #else
199
      #run_as_postgres sed -i -e "s|^[# ]*password_encryption[[:space:]]*=.*|password_encryption = scram-sha-256|g" "$POSTGRESQL_CONF"
200
      run_as_postgres sed -i -e 's/^\s*#\s*\(password_encryption\s*=\s*\).*$/\1scram-sha-256/g' "$POSTGRESQL_CONF"
201
   #fi
202
fi
203
run_as_postgres sed -i -e 's/^\s*#\(\s*log_destination\)\( *=\)\(.*\)$/\1\2\3/g' \
204
                       -e 's/^\s*#\(\s*logging_collector\)\( *=\)\(.*\)$/\1\2\3/g' \
205
                       -e 's/^\s*#\(\s*log_directory\)\( *=\)\(.*\)$/\1\2\3/g' \
206
                       -e 's/^\s*#\(\s*log_filename\)\( *=\)\(.*\)$/\1\2\3/g' \
207
                       -e 's/^\s*#\(\s*log_file_mode\)\( *=\)\(.*\)$/\1\2\3/g' \
208
                       -e 's/^\s*#\(\s*log_rotation_age\)\( *=\)\(.*\)$/\1\2\3/g' \
209
                       -e 's/^\s*#\(\s*log_rotation_size\)\( *=\)\(.*\)$/\1\2\3/g' \
210
                       -e "s/^\s*\(\s*max_connections\)\( *=\)\(.*\)$/\1\2 ${MAX_CONNECTIONS}/g" \
211
                       -e "s/^\s*\(\s*port\)\( *=\)\(.*\)$/\1\2 ${PGPORT}/g" \
212
                       -e "s/^\s*#\(\s*max_connections\)\( *=\)\(.*\)$/\1\2 ${MAX_CONNECTIONS}/g" \
213
                       -e "s/^\s*#\(\s*port\)\( *=\)\(.*\)$/\1\2 ${PGPORT}/g" \
214
                "$POSTGRESQL_CONF"
215

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

    
219
# ---- Configure pg_hba.conf ----
220
PG_HBA="${PGDATA}/pg_hba.conf"
221
echo "INFO: Configuring $PG_HBA with auth method: $AUTH_METHOD"
222
run_as_postgres sed -i -e "s/\(^local\s\+all\s\+all\s\+\)trust$/\1${AUTH_METHOD}/g" \
223
                       -e "s/\(^host\s\+all\s\+all\s\+127\.0\.0\.1\/32\s\+\)trust$/\1${AUTH_METHOD}/g" \
224
                       -e "s/\(^host\s\+all\s\+all\s\+\:\:1\/128\s\+\)trust$/\1${AUTH_METHOD}/g" \
225
                       -e "s/\(^local\s\+replication\s\+all\s\+\)trust$/\1${AUTH_METHOD}/g" \
226
                       -e "s/\(^host\s\+replication\s\+all\s\+127\.0\.0\.1\/32\s\+\)trust$/\1${AUTH_METHOD}/g" \
227
                       -e "s/\(^host\s\+replication\s\+all\s\+\:\:1\/128\s\+\)trust$/\1${AUTH_METHOD}/g" \
228
                   "$PG_HBA"
229
# Keep any existing lines, but ensure we have our standard baseline at the end
230
echo "host all all all $AUTH_METHOD" | run_as_postgres tee -a "$PG_HBA" > /dev/null
231

    
232
# Create a temporary pg_hba.conf with trust authentication
233
BOOTSTRAP_HBA="${PGDATA}/pg_hba.bootstrap.conf"
234
sudo tee "$BOOTSTRAP_HBA" > /dev/null <<'HBA'
235
# Bootstrap-only HBA (trust all local connections)
236
local   all   all                 trust
237
host    all   all   127.0.0.1/32  trust
238
host    all   all   ::1/128       trust
239
HBA
240
sudo chown -R postgres:postgres "$BOOTSTRAP_HBA"
241
sudo chmod 600 "$BOOTSTRAP_HBA"
242
sudo cp "${ROLEFILE_PATH}/create_roles.sql" /tmp
243
sudo chown postgres:postgres /tmp/create_roles.sql
244

    
245
# ---- Bootstrap with HBA locally (socket only) to create roles ----
246
pushd /tmp >/dev/null
247
echo "INFO: Starting temporary server for role creation..."
248
run_as_postgres ${pg_path}/pg_ctl -D "$PGDATA" \
249
  -o "-c listen_addresses='' -c unix_socket_directories='$SOCKET_DIR' -p $TEMP_PORT -c hba_file=$BOOTSTRAP_HBA" \
250
  -w start
251

    
252
# Setup 'set' variables
253
PSQL_ARGS=(
254
  --set=std_user="${STANDARD_USER}"
255
  --set=std_pass="${STANDARD_PASS}"
256
  --set=admin_user="${ADMIN_USER}"
257
  --set=admin_pass="${ADMIN_PASS}"
258
)
259
[[ -n "$POSTGRES_PASSWORD" ]] && PSQL_ARGS+=(--set=pg_pass="$POSTGRES_PASSWORD")
260

    
261
# Run external SQL file
262
echo "INFO: Creating roles..."
263
run_as_postgres psql -h "$SOCKET_DIR" -p "$TEMP_PORT" -U postgres -d postgres \
264
                     "${PSQL_ARGS[@]}" \
265
                     -v ON_ERROR_STOP=1 \
266
                     -f "/tmp/create_roles.sql"
267

    
268
# Stop temp server
269
echo "INFO: Stopping temporary server..."
270
run_as_postgres ${pg_path}/pg_ctl -D "$PGDATA" -w stop
271
popd >/dev/null
272

    
273
# Final ownership/permissions setup
274
if id postgres >/dev/null 2>&1; then
275
   sudo chown -R postgres:postgres "$PGDATA" || true
276
fi
277
sudo chmod 700 "$PGDATA" || true
278

    
279
# Cleanup bootstrap hba and roles file
280
sudo rm -f "$BOOTSTRAP_HBA" || true
281
sudo rm -f "/tmp/create_roles.sql" || true
282

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

    
286
if ${pg_path}/pg_ctl -D "$PGDATA" status >/dev/null 2>&1; then
287
   echo ✅ Cluster is running at: $PGDATA
288
else
289
# Show some useful information
290
   cat <<SUM
291

    
292
✅ Cluster is ready to run at: $PGDATA
293

    
294
Next steps:
295
- To run with the official postgres image:
296
    docker run -e POSTGRES_PASSWORD='${POSTGRES_PASSWORD:-<set>}' \\
297
               -v <volume_name>:"$PGDATA" \\
298
               -p $PGPORT:$PGPORT \\
299
               postgres:${PG_VERSION}
300

    
301
- Or start locally (outside Docker):
302
    as postgres:  ${pg_path}/pg_ctl -D "$PGDATA" -w start
303

    
304
Notes:
305
- listen_addresses='*' has been set. Adjust pg_hba.conf to restrict host access before exposing ports.
306
- Auth method in pg_hba.conf: $AUTH_METHOD
307
SUM
308
fi