Backups

How to backup the database, storage volume, and encryption secrets for Pro self-hosted — SQLite, PostgreSQL, uploads, and the install-generated .env.

This article applies to: Pro Self-Hosted

Backing Up the Pro Self-Hosted Database

Password Pusher Pro self-hosted uses different databases depending on your plan:

Plan Database
Starter SQLite3
Advanced SQLite3
Enterprise PostgreSQL

Follow the section that matches your deployment. In Docker setups, the database file or data directory is typically on a volume — ensure your backup process can access that path (e.g. run backup from inside the container or from the host if the volume is bind-mounted).


Backup set (what you must keep)

A database dump alone is not a full restore. Keep this set together, off the app host:

Piece Starter / Advanced Enterprise Why
.env Required Required Encryption and application secrets. Without it, restored data cannot be decrypted.
Storage volume (pwpush-pro-data/opt/PasswordPusher/storage) Required Required if using local file storage SQLite files (non-Enterprise) and uploaded files.
PostgreSQL Required App data lives in Postgres, not in the storage volume.
docker-compose.yml If you customized it If you customized it Recreates ports, volumes, and image tag.

If Admin → Settings → File Storage is S3, GCS, Azure Blob, or similar, file bytes live in that bucket. Back up or version the bucket as well; the storage volume then holds little or no upload data.

Default Compose uses a named volume. Named volumes sit in Docker’s volume directory, so host-disk backups often miss them unless you back the volume up explicitly (methods below).


Storage volume (uploads and local data)

The default install mounts:

volumes:
  - pwpush-pro-data:/opt/PasswordPusher/storage

That directory holds:

  1. File uploads when local storage is selected
  2. SQLite databases on Starter and Advanced (storage/db/production.sqlite3 and related files)
  3. Application temp files

Enterprise: still back up this volume if you store files locally. Postgres is a separate volume (pwpush-pro-postgres-data) — use PostgreSQL for the database.

From the install directory (service name pwpush-pro):

docker compose exec pwpush-pro tar czf /tmp/storage-backup-$(date +%Y%m%d).tar.gz -C /opt/PasswordPusher storage
docker compose cp pwpush-pro:/tmp/storage-backup-$(date +%Y%m%d).tar.gz ./storage-backup-$(date +%Y%m%d).tar.gz
docker compose exec pwpush-pro rm /tmp/storage-backup-$(date +%Y%m%d).tar.gz

Copy that archive off the server. For Starter/Advanced this includes SQLite and files, but a live tar of SQLite can be slightly inconsistent. For production, also take a SQLite .backup (or stop the app briefly, then tar).

Method 2: volume helper if the container is stopped

CONTAINER_ID=$(docker compose ps -aq pwpush-pro)

docker run --rm \
  --volumes-from "$CONTAINER_ID" \
  -v "$(pwd)":/backup \
  alpine tar czf /backup/storage-backup-$(date +%Y%m%d).tar.gz -C /opt/PasswordPusher storage

Method 3: bind mount on the host

If docker-compose.yml mounts a host path instead of a named volume, archive that path directly:

tar czf storage-backup-$(date +%Y%m%d).tar.gz /host/path/to/storage

Check with docker compose config or the volumes: section of docker-compose.yml.


SQLite (Starter & Advanced)

These methods create a consistent snapshot of your SQLite database, suitable for production and safe to run while the application is in use.

The safest and most official way to get a consistent snapshot while the database may still be in use.

From inside the app container (Docker Compose):

docker compose exec pwpush-pro sqlite3 /opt/PasswordPusher/storage/db/production.sqlite3 ".backup '/tmp/backup-$(date +%Y%m%d).db'"
docker compose cp pwpush-pro:/tmp/backup-$(date +%Y%m%d).db ./backup-$(date +%Y%m%d).db
docker compose exec pwpush-pro rm /tmp/backup-$(date +%Y%m%d).db

A .backup of the SQLite file is a consistent DB snapshot. It does not include uploaded files — keep a storage volume archive as well (or tar the whole storage directory after a short stop).

In Python (e.g. for a backup script or cron job):

import sqlite3
from datetime import datetime

def backup_sqlite(db_path: str, backup_dir: str):
    timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
    backup_path = f"{backup_dir}/backup_{timestamp}.db"

    source = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True)  # read-only is safest
    dest   = sqlite3.connect(backup_path)

    with dest:
        source.backup(dest)  # online backup API

    source.close()
    dest.close()
    print(f"Backup created: {backup_path}")

Advantages: Consistent snapshot with other connections writing; no need to stop the app; fast on local storage.

2. VACUUM INTO (production / defragmented backup)

Good when you also want a smaller, defragmented copy. Requires SQLite 3.27+.

sqlite3 /path/to/production.sqlite3 "VACUUM INTO '/backups/pwpush-2026-02-25-vacuumed.db';"

When to prefer over .backup: You want a smaller backup file, you already run periodic VACUUM, or you have high write concurrency.

3. Logical backup (.dump — SQL script)

Creates a portable, plain-text SQL file (schema + data).

sqlite3 /path/to/production.sqlite3 .dump > full-dump-$(date +%Y-%m-%d).sql
# Data only (no schema):
sqlite3 /path/to/production.sqlite3 .dump --data-only > data-only.sql

Best for: Moving between SQLite versions, auditing, long-term archives. Downsides: Slower to create and restore; larger unless gzipped.


PostgreSQL (Enterprise)

Enterprise deployments use PostgreSQL. Use logical backups for portability and simplicity, or physical/base backups if you need point-in-time recovery.

Creates a single file (custom, directory, or plain SQL). Run from a host that can connect to your Postgres (e.g. app container or admin host).

Custom format (compressed, flexible restore):

pg_dump -Fc -h <host> -U <user> -d <database_name> -f pwpush-$(date +%Y-%m-%d).dump

Plain SQL (human-readable, restorable with psql):

pg_dump -h <host> -U <user> -d <database_name> > pwpush-$(date +%Y-%m-%d).sql

Restore:

# Custom format
pg_restore -d <database_name> pwpush-2026-02-25.dump

# Plain SQL
psql -h <host> -U <user> -d <database_name> < pwpush-2026-02-25.sql

Use the same database name and credentials as your Pro app configuration.

From Compose (bundled db service):

docker compose exec db pg_dump -U pwpush-pro -Fc -d pwpush-pro -f /tmp/pwpush-$(date +%Y%m%d).dump
docker compose cp db:/tmp/pwpush-$(date +%Y%m%d).dump ./pwpush-$(date +%Y%m%d).dump

Adjust user/database if you changed them in .env. Also archive pwpush-pro-data if file uploads are on local disk.

2. Continuous backup and point-in-time recovery (PITR)

For minimal data loss, use PostgreSQL’s WAL archiving plus a base backup:

  1. Base backup (e.g. pg_basebackup or a consistent pg_dump).
  2. WAL archiving — set archive_mode=on and archive_command so WAL segments are copied to safe storage (e.g. S3, NFS).

Then you can restore to any point in time using the base backup plus replayed WAL. Configure this in your PostgreSQL server (or managed service) and test restores regularly.

3. Managed PostgreSQL (e.g. RDS, Cloud SQL)

If your Enterprise deployment uses a managed Postgres service, use its built-in backup and snapshot features and follow the provider’s restore and PITR documentation.


Best Practices (both SQLite and PostgreSQL)

Practice Why it matters Recommendation
Prefer online/safe methods .backup / VACUUM INTO for SQLite; pg_dump for Postgres Use by default
3-2-1 rule 3 copies, 2 different media types, 1 off-site Critical for production
Automate + timestamp Daily or hourly depending on how often data changes Strong
Test restores Backups are useless if restores fail Mandatory — use the restore checklist
Include the storage volume Named volumes are not in ordinary host backups; they hold SQLite and/or uploads Same schedule as the database
Store off the same disk Protects against disk failure and ransomware Very strong
Compress Use gzip or zstd — databases compress well Recommended
Encrypt if sensitive Use age, rclone crypt, or provider encryption If PII/financial
Don’t copy live WAL files manually (SQLite) Risk of corruption — use SQLite tools only Never

Quick decision guide:

  • Starter/Advanced (SQLite), simple setup → Storage-volume tar + .env off-site (includes SQLite and local files).
  • Starter/Advanced, production → SQLite .backup (or VACUUM INTO) plus a storage-volume tar for uploads + .env; consider Litestream for the database.
  • Enterprise (PostgreSQL) → Scheduled pg_dump (or managed backups) + storage-volume tar if files are local + .env; enable WAL archiving and PITR if you need minimal RPO.

Encryption keys and .env (all plans)

The install command you run after purchase (curl ... install.sh | sh) downloads docker-compose.yml and a newly generated .env file into your install directory. That .env is created with fresh secrets used for encryption and application security. Without those values, a database backup alone cannot decrypt push payloads or restore a working app—treat .env as part of your backup set.

What to back up

  • The whole .env file from the directory where you run docker compose (same folder as docker-compose.yml). It holds the generated keys and any other install-time configuration the script wrote.
  • Optionally docker-compose.yml if you customize it—so you can recreate the stack exactly.

Do not rely on only backing up volumes or the DB file if you might ever need to restore on a new host or after losing the install directory.

Best practices

Practice Why
Copy .env off the server Disk failure or ransomware on the host can wipe both DB and .env if they’re only on that machine.
Store with the same rigor as the database 3-2-1, off-site, encrypted at rest (e.g. password manager vault, encrypted backup, or secrets manager)—not in email or chat.
Never commit .env to git Prevents accidental leaks via repos or CI logs.
Restrict file permissions On the server, keep .env readable only by the account that runs Compose (e.g. chmod 600 .env).
Document where backups live During a restore, you need database + storage volume (local files) + .env (and compose file if customized).
Rotate keys only with a plan Changing encryption secrets without re-encrypting data can make existing ciphertext unreadable—follow product guidance before rotating.

Restore checklist

Practice this on a spare host or after a snapshot — before you need it.

Do not run docker compose down -v. The -v flag deletes named volumes.

  1. Install directory — Same layout as production: docker-compose.yml plus the same .env (keys must match the backup).
  2. Create volumesdocker compose up -d then docker compose stop so named volumes exist but nothing is writing.
  3. Restore data
    • Starter/Advanced: Unpack the storage archive into the volume (tarball was created with -C /opt/PasswordPusher storage):

      CONTAINER_ID=$(docker compose ps -aq pwpush-pro)
      docker run --rm \
        --volumes-from "$CONTAINER_ID" \
        -v "$(pwd)":/backup \
        alpine tar xzf /backup/storage-backup-YYYYMMDD.tar.gz -C /opt/PasswordPusher
      

      If you used SQLite .backup instead of a full volume tar, copy that file to storage/db/production.sqlite3 and still restore uploaded files from a storage archive.

    • Enterprise: Restore Postgres (pg_restore / psql, or restore pwpush-pro-postgres-data) and restore pwpush-pro-data if you used local file storage. Point DATABASE_URL at the restored database.
    • Cloud file storage: Confirm bucket credentials in Admin settings still reach the same bucket.
  4. Startdocker compose up -d
  5. Healthdocker compose ps should show healthy; curl -f http://127.0.0.1/up (or your published HTTP port) returns 200. See Operations — Healthcheck.
  6. Smoke test — Sign in as an admin. Open a known push. If you use file pushes, download an attachment.
  7. If payloads look empty or decrypt fails.env does not match this database. Stop, restore the correct .env, start again.

For background on what the keys protect, see Application encryption (OSS doc; concepts align with how Pro uses secrets at install time).


See Also