Runbook: Deploy and Hand Over a Production Cluster
1 · Prerequisites
Confirm every item is in place before any state change.
- The host, its storage layout, and confirmation of how much of it is dedicated to this cluster
- The locale and encoding the applications require, because these are fixed at initdb time and changing them means a dump and reload
- The intended topology: standalone, primary with standbys, and whether any standby is synchronous
- Storage for backups and for the WAL archive, reachable from this host, with capacity for the retention you intend
- The application inventory: which databases, which roles, and what each needs to do
- A named owner for the cluster, and agreement on the recovery time and recovery point objectives
2 · Pre-checks
Read-only diagnostic commands. If any of these don't match expected output, stop and investigate further.
- · Confirm the storage layout before initdb. A separate filesystem for
pg_walmeans a full WAL volume stops only the cluster rather than everything sharing the disk, and it makes WAL growth a visible metric of its own. Adding it later means moving the directory. - · Decide encoding and locale, and write down why.
UTF8with an appropriate collation is the usual answer. These are fixed at initdb time; changing them later requires a dump and reload of the whole cluster. - · Confirm data checksums will be on. PostgreSQL 18 enables them by default. Without them a damaged page is returned as fact with no error and no log entry;
checksum_failurescannot increment on a cluster that computes no checksums. - · **Size
shared_buffersand the memory budget as an arithmetic exercise.**shared_buffersplusmax_connections x work_mem x memory_nodes_per_planplus the operating system's needs, against physical RAM. If nobody can produce that number, the memory settings have been guessed. - · Confirm the backup destination and the archive destination exist and are writable by the postgres user on this host, tested by hand.
- · Confirm the monitoring system can reach this host and that somebody has agreed to receive its alerts.
- · Agree the recovery objectives before building anything. They determine the base backup cadence, the archive retention and whether a standby is required, and retrofitting them is more expensive than designing for them.
3 · Procedure
Execute each step in order. Verify the expected output of a step before moving to the next.
- 1Run initdb with the decisions made explicitly, rather than relying on defaults you have not read:
initdb -D /var/lib/postgresql/18/main --encoding=UTF8 --locale=... --data-checksums --waldir=/var/lib/postgresql/18/wal - 2Confirm what initdb produced.
pg_controldata -D <datadir>and check the encoding, the locale, andData page checksum version. This is the last cheap moment to correct any of them. - 3Set the connection and authentication configuration before the cluster is reachable.
listen_addresses,port, and apg_hba.confwith notrustrule for any address a real client can reach. Loopbacktrustis present in most default installations and it makes every local test pass unconditionally. - 4Enable TLS before any client connects. Install the certificate material, set
ssl = on, and usehostsslrather thanhostinpg_hba.conf. Retrofitting enforcement onto a live estate means cutting off whatever is not yet configured. - 5Set the durability and WAL configuration.
wal_level = replicaat minimum,archive_mode = onwith a workingarchive_command, and amax_wal_sizesized from the expected write rate rather than left at a default nobody chose. - 6Set the logging configuration so the cluster can be diagnosed.
log_checkpoints,log_connections,log_disconnections,log_lock_waits,log_temp_files,log_autovacuum_min_duration, and alog_line_prefixthat includes the timestamp, the process id, the user and the database. - 7**Set
log_min_duration_statementdeliberately.** Too low and the log is unusable; too high and slow queries are invisible. Choose a value from the application's expected latency. - 8**Install
pg_stat_statements** and add it toshared_preload_libraries. It requires a restart, which is free now and a change request later. - 9Create the databases and roles, using group roles for privileges and login roles for clients, with
ALTER DEFAULT PRIVILEGESset for the owning role so future objects inherit. - 10Take a base backup and restore it, on a different host, before the cluster carries any production data. The restore time you measure is the recovery time objective the estate actually has.
- 11Build the standby, if the topology calls for one, and rehearse a promotion. A failover target that has never been promoted is a hypothesis.
- 12Configure monitoring and test each alert by causing the condition: stop the standby, pause replay, fill a volume in a test, exhaust the connections. An alert that has never fired has never been shown to work.
- 13Hand over with the evidence, not with a document. The restore that was performed, the failover that was rehearsed, the alerts that were tested, and the timings for each.
4 · Verification
Confirm the procedure actually fixed the problem.
- ✓
pg_controldatareports the intended encoding, locale andData page checksum version: 1. - ✓
SELECT name, setting FROM pg_settings WHERE source <> 'default' ORDER BY name;matches the intended configuration exactly, with nothing set that nobody chose. - ✓
SELECT rule_number, type, database, user_name, address, auth_method FROM pg_hba_file_rules ORDER BY rule_number;contains notrustrule for any address a real client can reach. - ✓A
verify-fullTLS client connects by the name in the certificate, and a client using a different name is refused with a hostname mismatch. - ✓A base backup has been taken and restored on another host, with the restore time recorded.
- ✓
pg_stat_archivershows successful archiving andfailed_countat zero, confirmed by forcing a segment withpg_switch_wal()and checking it arrived. - ✓If a standby exists, it has been promoted once in a rehearsal and rejoined, with each step timed.
- ✓Every alert has been fired once, deliberately, and the resulting page reached a human.
- ✓A named owner is recorded, along with the agreed recovery objectives and the measured numbers against them.
5 · Rollback
If verification fails, undo the procedure in reverse order.
- ↶Before the cluster carries data, rollback is to stop it and remove the data directory. Do that rather than reconfiguring around a decision that was wrong at initdb time.
- ↶Encoding, locale and the WAL directory location are effectively fixed once data exists. Correcting them means a dump and reload, so correct them now.
- ↶Data checksums can be enabled later with
pg_checksums --enableon a stopped cluster — measured at 159 ms on a 43 MB database, scaling with size — so this one is recoverable, but it is free at initdb time. - ↶If the cluster has been handed over and a configuration decision proves wrong, treat it as an ordinary configuration change with its own change note, rather than as a correction to this procedure.
- ↶If the handover was accepted without the restore, the failover rehearsal or the alert tests, record that explicitly as an open item with an owner. An undone verification that nobody wrote down becomes an assumption.
- ↶If the cluster is being abandoned before production, follow the decommission procedure rather than deleting it — the backup destination and the archive may already hold data.
6 · Escalation
When the runbook isn't enough, contact:
- · The recovery objectives cannot be met with the storage and topology available: escalate to the service owner with the measured restore time. The objective and the budget are both theirs to reconcile.
- · The application requires an encoding or collation that conflicts with the estate standard: escalate before initdb. This is the one decision that cannot be changed without a full dump and reload.
- · No backup destination is available: escalate and do not proceed to production. A cluster without a tested restore is not a production cluster regardless of what else is configured.
- · Nobody will accept ownership of the cluster: escalate to the platform owner. An unowned cluster accumulates every problem in this course and nobody is paged for any of it.
- · The memory arithmetic does not fit the host: escalate rather than setting values that work most of the time.
work_memis per operation, and the worst case is what the out-of-memory killer acts on. - · A required extension is not packaged for this platform: escalate to whoever owns the platform before building. An extension installed from source on a production host is a maintenance liability with no owner.
Most of what makes a cluster good to operate is decided in the first ten minutes, and some of it cannot be changed afterwards without a dump and reload.
Fixed at initdb time
Give pg_wal its own filesystem
A full WAL volume stops the cluster either way. On its own filesystem it stops only the cluster, rather than everything sharing the disk — and its growth becomes a metric of its own rather than a component of one.
Size it for max_wal_size plus retention, not for max_wal_size.
Unarchived segments and replication slots are retained regardless of that
setting, and a volume sized exactly to it has no margin at all.
The security defaults that are not secure
And enable TLS before the first client connects. Retrofitting hostssl
onto a live estate means cutting off whatever is not yet configured;
doing it at build time costs nothing.
Logging that makes the cluster diagnosable
Half the incidents in this course are diagnosed from a log line that was never enabled. Turn these on at build time:
| Setting | What it makes visible |
|---|---|
log_checkpoints | WAL-driven checkpoints and occurring too frequently |
log_connections / log_disconnections | Which pg_hba.conf rule matched, and by whom |
log_lock_waits | Lock queues, with the blocker named |
log_temp_files | Which queries spill, and how much |
log_autovacuum_min_duration | Vacuum completions, so they can be compared with starts |
log_min_duration_statement | Slow queries, at a threshold chosen from the application |
Note that log_lock_waits has a boot value of off in PostgreSQL 18.6 —
verified against pg_settings.boot_val, not assumed.
Install pg_stat_statements and add it to shared_preload_libraries
while a restart is free.
Blast radius
| Action | Reversible? | What it costs if wrong |
|---|---|---|
initdb before data exists | Yes | Minutes |
| Encoding or locale after data exists | Effectively no | A dump and reload of the cluster |
pg_wal location after data exists | Stop and move | An outage |
Leaving trust in pg_hba.conf | Yes | Every verification anybody performs locally |
| Handing over without a tested restore | Deferred | The recovery capability nobody has |
Hand over with evidence, not with a document
And name an owner. An unowned cluster accumulates every problem in this course, and nobody is paged for any of them.