Rsync
You can use rsync
if need to move your PostgreSQL data directory to another
location. This is useful if you are running out of space on the current disk or
if you want to move the data directory to a faster disk.
This can be done without stopping the PostgreSQL server, the steps are as follows:
rsync
the data directory to the new location.- Once almost all the data is copied, you can either stop the PostgreSQL server (advised) and sync the ramaining data or take a
pg_backup_start
, rsync the remaining data and then take apg_backup_stop
and then sync again. - update the
data_directory
parameter inpostgresql.conf
of the new location - start the PostgreSQL server
Here is an example of how to do this, from the active server to a new server:
rsync -aHAXx --numeric-ids --delete --info=progress2 --inplace --partial \
-e "ssh -T -c [email protected] -o Compression=no -x" \
--exclude postmaster.pid \
--exclude postmaster.opts \
/opt/postgres/16 \
newserver:/opt/postgres/
You may also exclude the pg_ha.conf, pg_indent.conf and the postgresql.conf if you want to keep the old configuration files.
The ssh options explained (This often doubles throughput on LANs):
-T disables SSH pseudo-tty (tiny win).
-c [email protected] uses a very fast cipher (assuming OpenSSH ≥ 6.2).
-o Compression=no avoids double compression overhead.
-x disables X11 forwarding (tiny win).
Once the rsync is almost done, create the backup label and sync again:
SELECT pg_backup_start('rsync_migration', true);
The
true
will try to create the checkpoint faster
This is useful because PostgreSQL does not flush everything instantly to disk; many pages live in shared buffers and are written to disk later. By taking a backup label, you ensure that the data is in a consistent state.
pg_backup_start
performs a checkpoint, starts a full-page write in WAL, and creates a backup label file in the data directory. This ensures that the data copied is consistent and can be used for recovery if needed.
Then run the rsync again to copy the remaining data and when finished run:
SELECT pg_backup_stop();
Save the output of pg_backup_stop()
, as it contains the backup_label and the
WAL segment name needed to make the backup consistent. This tells you which WAL
files must be copied to the destination for recovery. The output looks like:
postgres=# SELECT pg_backup_stop();
NOTICE: all required WAL segments have been archived
pg_backup_stop
-----------------------------------------------------------------------------------
(20E0/7B000138,"START WAL LOCATION: 20E0/7B000028 (file 00000008000020E00000007B)+
CHECKPOINT LOCATION: 20E0/7B000060 +
BACKUP METHOD: streamed +
BACKUP FROM: primary +
START TIME: 2025-08-27 13:51:42 UTC +
LABEL: rsync_migration +
START TIMELINE: 8 +
","")
(1 row)
In the new server create the backup_label
file in the new data directory $PGDATA
with the content from the output of pg_backup_stop()
., from the example above it would be:
START WAL LOCATION: 20E0/7B000028 (file 00000008000020E00000007B)
CHECKPOINT LOCATION: 20E0/7B000060
BACKUP METHOD: streamed
BACKUP FROM: primary
START TIME: 2025-08-27 13:51:42 UTC
LABEL: rsync_migration
START TIMELINE: 8
Opionaly, run the rsync
again to copy the remaining data.
Finally, update the data_directory
parameter in postgresql.conf
on the new server, ensure the backup_label
file is present in the new data directory, and start PostgreSQL:
Script
Here is a script that automates the process:
#!/bin/bash
set -euo pipefail
SRC_PGDATA="/var/lib/postgresql/15/main"
DST_HOST="newserver"
DST_PGDATA="/var/lib/postgresql/15/main"
PGUSER="postgres"
RSYNC_OPTS="-aHAXx --numeric-ids --delete \
--exclude postmaster.pid \
--exclude postmaster.opts \
--exclude pg_hba.conf \
--exclude pg_ident.conf \
--exclude postgresql.conf"
echo "[*] Pre-syncing data directory (this may take a while)..."
rsync $RSYNC_OPTS "$SRC_PGDATA/" "$DST_HOST:$DST_PGDATA/"
echo "[*] Starting PostgreSQL backup mode..."
psql -U $PGUSER -d postgres -c "SELECT pg_backup_start('rsync_migration', true);"
echo "[*] Syncing data directory again (this should take less time)..."
rsync $RSYNC_OPTS "$SRC_PGDATA/" "$DST_HOST:$DST_PGDATA/"
echo "[*] Stopping PostgreSQL backup mode and capturing label..."
backup_info=$(psql -U $PGUSER -d postgres -t -A -c "SELECT pg_backup_stop();")
# Extract the formatted part from the output
backup_label=$(echo "$backup_info" | sed 's/^[ (]\|[",)]$//g')
# Write the label into a temp file
tmpfile=$(mktemp)
echo "$backup_label" | tr '+' '\n' > "$tmpfile"
echo "[*] Last sync of data directory..."
rsync $RSYNC_OPTS "$SRC_PGDATA/" "$DST_HOST:$DST_PGDATA/"
echo "[*] Installing backup_label on destination..."
scp "$tmpfile" "$DST_HOST:$DST_PGDATA/backup_label"
rm -f "$tmpfile"
echo "[*] Fixing ownership on destination..."
ssh "$DST_HOST" "sudo chown -R postgres:postgres $DST_PGDATA"
echo
echo "[*] Migration complete!"
echo "Now you can start PostgreSQL on $DST_HOST with:"
echo " pg_ctl -D $DST_PGDATA start"
echo
echo "Don't forget to update postgresql.conf if needed."