Migrating PostgreSQL Databases Between Servers Using Docker

Migrating PostgreSQL databases between servers can be tricky when client and server versions don’t align. Using Docker, you can run the exact PostgreSQL client version you need for pg_dump and pg_restore without installing multiple versions on your system. This guide walks through installing Docker, checking versions, performing dumps and restores without creating roles or databases, and using automated version-smart scripts for smooth migrations.

Table of Contents

PostgreSQL migrations often stumble when the client utilities (pg_dump, pg_restore, psql) don’t match the server version. With Docker, you can run the exact client version you need—no multi-version installs on your host, no PATH conflicts, and fewer surprises.

This guide walks you through:

  • Installing Docker on common Linux distributions
  • Checking PostgreSQL server versions to pick the right client
  • Dumping a database without creating the database or roles
  • Restoring that dump on another server
  • Using drop-in scripts (dump.sh, restore.sh) that:
    • auto-detect the server’s major version and select postgres:<major>
    • use ~/.pgpass or PGPASSWORD, and prompt only if needed
    • enforce a connect timeout and default to SSL (great for RDS)
    • produce custom-format dumps for fast, parallel restores

Get the newest scripts here (source of truth):
https://github.com/reliablepenguin/rp_pg_utils


Install Docker

What’s happening here? We’re installing the Docker engine so we can run the official postgres containers locally. We also enable the service so Docker starts automatically after reboot.

Ubuntu / Debian

AlmaLinux / Rocky Linux / CentOS

Fedora

Verify

Why verify? It confirms Docker is installed and reachable from your shell. If this fails, check that your user is allowed to run Docker (you may need to add yourself to the docker group and re-login).


Why “version-smart” matters

Concept: The PostgreSQL client tools are somewhat forward/backward compatible, but the safest path is to use the same major version as the server you’re talking to.

  • Use the source server’s major version for pg_dump (e.g., PG 15 server ⇒ postgres:15) so the dump is produced using exactly the features that server expects.
  • Use the destination server’s major version for pg_restore (e.g., PG 16 server ⇒ postgres:16) so the restore tool understands how to apply objects on that target.

How we do it: The scripts query SHOW server_version_num; (e.g., 150007) and compute the major (15). That number becomes the Docker image tag we run (postgres:15).


Manual Steps (if you don’t want scripts)

These are “raw” one-liners for folks who prefer manual control. The scripts later automate the same flow but handle version detection, timeouts, and credentials for you.

Step 1: Check server versions

What this does: Runs psql inside a container to connect to each server and print its reported version. You can also read the numeric variant for precise major/minor math.

Source:

Destination:

Why this matters: Knowing the exact versions helps you pick the right postgres:<major> tag (e.g., postgres:15), so your client tools match the server. If your server enforces SSL (e.g., RDS), add sslmode=require via a connection string (the scripts do this automatically).


Step 2: Dump the database (no CREATE DATABASE, no roles)

What this does: Runs pg_dump inside a container that matches the source server version. We use custom format so we can do a parallel, selective restore later. We explicitly do not include CREATE DATABASE or any roles—this keeps the dump portable and safe to restore into an already-created DB.

Custom format (recommended):

Plain SQL (optional):

Flag explanations:

  • -F c = custom format (compressed, supports pg_restore --jobs)
  • --no-owner --no-privileges = strip GRANT/OWNER; you’ll re-grant as needed
  • No -C = no CREATE DATABASE in the output
  • Not using pg_dumpall -g = roles are excluded on purpose

Step 3: Transfer the dump file

What this does: Copies your dump to the box from which you’ll run the restore. Use whatever you like—scp, rsync, S3, etc.

Tips: For very large dumps, prefer a path with low latency to the DB (e.g., same region/AZ). The custom format is already compressed; for plain SQL, consider compressing before transfer.


Step 4: Prepare the destination

What this does: Ensures the destination database exists and optionally sets schema ownership so your app user owns the default schema. If the DB already exists, you can skip creation.

Create DB:

(Optionally) set schema owner:

Why do this now? Since the dump does not contain CREATE DATABASE, the target DB must exist. Ownership adjustments ensure your app user can write to the schema as expected.


Step 5: Restore

What this does: Uses pg_restore inside a container that matches the destination server version. We restore with --jobs to parallelize larger databases. We also tell pg_restore not to change owners or privileges; apply the policy you want afterwards.

Custom format restore:

Plain SQL restore:

Flags to note:

  • --jobs=4 = parallelize restore (tune for your CPU/IO)
  • --disable-triggers = speed things up at the risk of more load; often OK for one-off migrations
  • --no-owner --no-privileges = leave ownership/GRANTs alone; set them explicitly after

Automated Scripts (recommended)

Always check the repo for the latest:
https://github.com/reliablepenguin/rp_pg_utils

What these do:

  • Detect the server’s major version and choose postgres:<major> automatically
  • Use ~/.pgpass if present (mounted into the container), or PGPASSWORD if set
  • If neither is available, prompt once and pass the password to the container
  • Use a libpq connection string with connect_timeout=10 and sslmode=require (tweak PGSSLMODE if your target doesn’t require SSL)
  • Avoid pseudo-TTY quirks and print verbose progress so you can see what’s happening

dump.sh

restore.sh


Usage examples

What this shows: Typical invocations. The scripts prompt for a password only when neither ~/.pgpass nor PGPASSWORD is available. They print which client image they used and where the dump went.

Dump

Restore


~/.pgpass quick reference

What/why: A local credentials file that libpq (Postgres client library) reads automatically. It prevents interactive prompts and keeps passwords out of your shell history.

  • Location: ~/.pgpass (permissions 0600)
  • Format: host:port:database:username:password

Example (AWS RDS-style endpoint):


Troubleshooting (what’s going on and how to fix)

“It hangs on connect.”
We set connect_timeout=10 in the connection string, so hard hangs usually mean network or SSL mismatches. Verify security groups/NACL, DNS, and SSL policy.
Raw test:

“Auth fails despite .pgpass.”
Libpq ignores .pgpass unless it’s 0600. The line must match exactly (host, port, db, user). For RDS, ensure you’re using the correct endpoint (cluster vs instance endpoint can differ).

“Restore is slow.”
Increase --jobs and ensure you’re on a machine close to the DB (same region/AZ). Watch for constraints/locks—--disable-triggers can help but increases load.

“Ownership/GRANTs missing after restore.”
By design we use --no-owner --no-privileges. Apply your intended grants/roles explicitly post-restore—that’s safer and more repeatable.


Keep current: GitHub repo

We’ll keep improving these utilities over time. For the latest versions and any fixes, get them from the repo:

https://github.com/reliablepenguin/rp_pg_utils

Have a project or a problem?

Talk with a senior engineer for practical recommendations—no obligation.

Leave a comment

Leave a Reply

Your email address will not be published. Required fields are marked *

Related Posts

Categories

Get a free consultation from Reliable Penguin

Submit the form—or for immediate service call 866-649-7984.