diff --git a/README.md b/README.md
index 0aba12cae3013ed9244858f0d9be202249b94862..96b851ae573b41b71fe018e48156db137da82408 100644
--- a/README.md
+++ b/README.md
@@ -141,6 +141,15 @@ rwm restic snapshots
 rwm restic mount /mnt/restore
 ```
 
+### Restore PostgreSQL backup
+
+```
+# Using script for the whole archive
+scripts/restore_postgresql.sh postgresql.tar.gz
+
+# For individual dumps
+createdb database_name && gunzip -c database_name.sql.gz | psql -d database_name
+```
 
 ## Notes
 
diff --git a/examples/rwm-backups.conf b/examples/rwm-backups.conf
index 07464d2a9393a34eb4dee54cd27827a84bd65b47..44dce21e95f1f5cdae1050f8e30119d53746d5f3 100644
--- a/examples/rwm-backups.conf
+++ b/examples/rwm-backups.conf
@@ -31,6 +31,16 @@ backups:
     postrun:
       - "/opt/rwm/scripts/backup_mysql.py cleanup"
 
+  postgresql:
+    filesdirs:
+      - /var/lib/rwm/postgresql.tar.gz
+    tags:
+      - "postgresql"
+    prerun:
+      - "/opt/rwm/scripts/backup_postgresql.py create"
+    postrun:
+      - "/opt/rwm/scripts/backup_postgresql.py cleanup"
+
 retention:
   keep-daily: "60"
   keep-within: "60d"
diff --git a/scripts/backup_postgresql.py b/scripts/backup_postgresql.py
new file mode 100755
index 0000000000000000000000000000000000000000..c6a5a4b0abb3f8ba38660bb5d39ea140b6ae3aa3
--- /dev/null
+++ b/scripts/backup_postgresql.py
@@ -0,0 +1,102 @@
+#!/usr/bin/env python3
+"""rwm postgresql backup helper"""
+
+import gzip
+import os
+import shutil
+import subprocess
+import sys
+from argparse import ArgumentParser
+
+
+BASE = "/var/lib/rwm"
+BACKUPDIR = f"{BASE}/postgresql"
+ARCHIVE = f"{BASE}/postgresql.tar.gz"
+USERNAME = os.environ.get("PGUSER", "postgres")
+
+
+def list_databases():
+    """list postgresql databases"""
+
+    cmd = ["psql", "-U", USERNAME, "-h", "127.0.0.1", "-q", "-A", "-t", "-c", "SELECT datname FROM pg_database WHERE datistemplate = false;"]
+
+    proc = subprocess.run(cmd, check=True, stdout=subprocess.PIPE, text=True)
+
+    return proc.stdout.splitlines()
+
+
+def backup_database(database):
+    """backup single database and compress it"""
+
+    cmd = ["pg_dump", "-U", USERNAME, "-h", "127.0.0.1", database]
+
+    try:
+        with gzip.open(f"{BACKUPDIR}/{database}.sql.gz", "wb") as fd:
+            with subprocess.Popen(cmd, stdout=subprocess.PIPE, universal_newlines=True) as proc:
+                for stdout_line in iter(proc.stdout.readline, ''):
+                    fd.write(stdout_line.encode('utf-8'))
+
+                # Dumps roles
+                if database == "postgres":
+                    roles_cmd = ["pg_dumpall", "-U", USERNAME, "-h", "127.0.0.1", "--roles-only"]
+                    with subprocess.Popen(roles_cmd, stdout=subprocess.PIPE, universal_newlines=True) as roles_proc:
+                        for stdout_line in iter(roles_proc.stdout.readline, ''):
+                            fd.write(stdout_line.encode('utf-8'))
+
+    except subprocess.CalledProcessError:
+        print(f"ERROR: cannot dump {database}", file=sys.stderr)
+        return 1
+
+    return 0
+
+
+def create():
+    """dump database to archive"""
+
+    databases = 0
+    errors = 0
+
+    shutil.rmtree(BACKUPDIR, ignore_errors=True)
+    os.makedirs(BACKUPDIR, exist_ok=True)
+
+    for db in list_databases():
+        databases += 1
+        errors += backup_database(db)
+
+    subprocess.run(["tar", "czf", ARCHIVE, BACKUPDIR], check=True)
+    shutil.rmtree(BACKUPDIR)
+    print("archive created:")
+    subprocess.run(["ls", "-l", ARCHIVE], check=True)
+
+    if databases == 0:
+        print("ERROR: no databases dumped", file=sys.stderr)
+        errors += 1
+
+    print(f"RESULT: errors={errors} databases={databases}")
+    return errors
+
+
+def cleanup():
+    """cleanup backup process"""
+
+    return os.unlink(ARCHIVE)
+
+
+# pylint: disable=duplicate-code
+def main():
+    """main"""
+
+    parser = ArgumentParser()
+    parser.add_argument("command", choices=["create", "cleanup"])
+    args = parser.parse_args()
+    os.umask(0o077)
+
+    if args.command == "create":
+        return create()
+    if args.command == "cleanup":
+        return cleanup()
+    return 1
+
+
+if __name__ == "__main__":
+    sys.exit(main())
diff --git a/scripts/restore_postgresql.sh b/scripts/restore_postgresql.sh
new file mode 100755
index 0000000000000000000000000000000000000000..ced0809bd22f91e4f51f41c9b664b956a14ec404
--- /dev/null
+++ b/scripts/restore_postgresql.sh
@@ -0,0 +1,31 @@
+#!/bin/bash
+
+if [ "$#" -ne 1 ]; then
+    echo "Usage: $0 <tar_file>"
+    exit 1
+fi
+
+tar_file="$1"
+
+if [ ! -f "$tar_file" ]; then
+    echo "Error: '$tar_file' does not exist."
+    exit 1
+fi
+
+temp_dir=$(mktemp -d)
+
+tar -xzf "$tar_file" -C "$temp_dir"
+
+sql_files=$(find "$temp_dir" -type f -name '*.sql.gz')
+
+for dump_file in $sql_files; do
+    db_name=$(basename "$dump_file" .sql.gz)
+    
+    createdb "$db_name"
+
+    gunzip -c "$dump_file" | psql -q -d "$db_name"
+done
+
+rm -rf "$temp_dir"
+
+echo "Databases restored."