diff --git a/postgres/Dockerfile b/postgres/Dockerfile
index f85f511f34fbf241696aaf1c458e786ead6debcd..3a936693925f6e57ecfcc30a316749cad3ece5f4 100644
--- a/postgres/Dockerfile
+++ b/postgres/Dockerfile
@@ -1,3 +1,3 @@
 FROM docker.io/postgres:16.1-alpine3.19
-COPY --chown=root:root healthcheck /usr/local/bin/
+COPY --chown=root:root healthcheck backup restore /usr/local/bin/
 HEALTHCHECK --start-period=3m CMD healthcheck
diff --git a/postgres/README.md b/postgres/README.md
index 42e8063ed98a321e612e71cba4c2e6522c65c563..0aefb5235b2461440e275fa897f61877bb4f2285 100644
--- a/postgres/README.md
+++ b/postgres/README.md
@@ -1,3 +1,27 @@
 # postgres
 
 Just the upstream image but with a healthcheck.
+
+## Backups
+
+The image includes a `backup` and `restore` scripts. The `backup` scripts dumps
+all of the databases using `pg_dumpall` and compresses the output using `zstd`
+to stdout. This is meant so that backups are run by an external process and it
+saves the output to a file, for example:
+
+```
+docker exec pg1 backup > /var/backups/pg1/dump.sql.zstd
+```
+
+The `restore` script matches the `backup` script in that the it reads a zstd
+compress SQL dump from stdin. An example restore:
+
+```
+cat dump.sql.zstd | docker exec -i pg2 restore
+```
+
+In fact you're able to migrate data from 1 instance to another like so:
+
+```
+docker exec pg1 backup | docker exec -i pg2 restore
+```
diff --git a/postgres/backup b/postgres/backup
new file mode 100755
index 0000000000000000000000000000000000000000..44099e81aefec56bdd551b13649866c7978ac702
--- /dev/null
+++ b/postgres/backup
@@ -0,0 +1,6 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+export PGUSER="${POSTGRES_USER:-postgres}"
+
+pg_dumpall | zstd
diff --git a/postgres/restore b/postgres/restore
new file mode 100755
index 0000000000000000000000000000000000000000..5aefec206a60e68f8e8e3e5287ee8fc93d83fa30
--- /dev/null
+++ b/postgres/restore
@@ -0,0 +1,6 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+export PGUSER="${POSTGRES_USER:-postgres}"
+
+zstd --decompress - | psql