Paperless-ngx Backup & Restore: The Right Strategy

How to create a secure, incremental backup of your Paperless-ngx instance using Docker and restore it in case of an emergency.

Paperless-ngx Backup & Restore: The Right Strategy hero image

A Document Management System (DMS) like Paperless-ngx is the heart of the paperless office. But what happens if the server fails, an update goes wrong, or the database becomes corrupt? Without a proper backup, not only are your settings lost, but in the worst-case scenario, your important documents are gone too.

I want to show you a robust backup strategy that secures both the individual database and the files โ€“ all while saving space and being fully automated.

Why simply copying folders isnโ€™t enough

Many users simply copy the entire Docker folder. This might work, but it carries risks:

  1. Database Consistency: If you copy the database files (PostgreSQL) while the container is running, the backup may be corrupt and unusable.
  2. Internal Links: Paperless links documents in the database with files in the file system. If these are not backed up synchronously, problems will arise.

The cleanest solution is a combination of an SQL Dump of the database and the integrated Document Exporter from Paperless.

The Strategy

We want to achieve the following:

  1. Create a clean dump of the PostgreSQL database.
  2. Convert the documents into a processable format using the Paperless Exporter (this also writes metadata into json files).
  3. Back up this data incrementally to save disk space (we donโ€™t want to copy 10 GB every day if only two PDFs have changed).
  4. Automatically delete old backups (Retention Policy).

Preparation

Create a file on your server, e.g., backup-paperless.sh.

nano /home/deployn/paperless/backup-paperless.sh

Paste the following content and adjust the variables at the top to match your paths:

#!/bin/bash

set -e
set -o pipefail

# --- CONFIGURATION ---
# Path where backups should be stored
BACKUP_BASE_DIR="/home/deployn/backup"
# Your Paperless directory (where the compose.yaml is located)
PAPERLESS_DIR="/home/deployn/paperless"

# Name of the symlink for the latest backup
LATEST_LINK="${BACKUP_BASE_DIR}/latest"

# Container names (check with 'docker container ls')
PG_CONTAINER="paperless-db"
WEBSERVER_CONTAINER="paperless-webserver"

# Database credentials (from your docker-compose.yml)
PG_USER="paperless"
PG_DB="paperless"

# How many days should backups be kept?
RETENTION_DAYS=30

DOCKER_CMD="/usr/bin/docker"
DOCKER_COMPOSE_CMD="/usr/bin/docker compose"

echo "========================================================"
echo "Starting Paperless Backup: $(date)"
echo "========================================================"

# 1. Create new backup directory
DATE_STAMP=$(date +"%Y-%m-%d_%H-%M-%S")
BACKUP_DIR="${BACKUP_BASE_DIR}/${DATE_STAMP}"
mkdir -p "${BACKUP_DIR}"
echo "[+] Creating backup directory: ${BACKUP_DIR}"

# 2. Backup PostgreSQL database
echo "[+] Creating PostgreSQL Dump..."
${DOCKER_CMD} exec "${PG_CONTAINER}" pg_dump -U "${PG_USER}" -d "${PG_DB}" > "${BACKUP_DIR}/paperless-db.sql"

# Check if the dump is empty
if [ ! -s "${BACKUP_DIR}/paperless-db.sql" ]; then
    echo "ERROR: Database dump is empty!"
    rm -rf "${BACKUP_DIR}"
    exit 1
fi
echo "    -> Database dump successful."

# 3. Export Paperless documents
echo "[+] Starting document_exporter..."
cd "${PAPERLESS_DIR}"
${DOCKER_COMPOSE_CMD} exec -T "${WEBSERVER_CONTAINER}" document_exporter ../export
echo "    -> Document exporter executed successfully."

# 4. Backup documents incrementally using rsync and hard links
echo "[+] Synchronizing documents..."

if [ -d "${LATEST_LINK}" ]; then
  LINK_DEST_OPTION="--link-dest=${LATEST_LINK}/documents"
  echo "    -> Previous backup found. Using hard links."
else
  LINK_DEST_OPTION=""
  echo "    -> No previous backup found. Creating full copy."
fi

# Assumption: ./export is mounted in PAPERLESS_DIR
rsync -a --delete ${LINK_DEST_OPTION} "${PAPERLESS_DIR}/export/" "${BACKUP_DIR}/documents/"
echo "    -> Documents synchronized."

# 5. Update 'latest' symlink
echo "[+] Updating 'latest' link."
ln -snf "${BACKUP_DIR}" "${LATEST_LINK}"

# 6. Delete old backups
echo "[+] Deleting backups older than ${RETENTION_DAYS} days..."
find "${BACKUP_BASE_DIR}" -maxdepth 1 -type d -not -name "latest" -mtime +${RETENTION_DAYS} -exec rm -rf {} \;
echo "    -> Cleanup completed."

echo "========================================================"
echo "Paperless backup successfully completed!"
echo "========================================================"

Make the script executable

For the script to run, we need to give it the appropriate permissions:

chmod +x /home/deployn/paperless/backup-paperless.sh

Important: The Export Path

The script assumes that you have defined a volume for the export in your compose.yaml. It should look something like this:

volumes:
    - ./data:/usr/src/paperless/data
    - ./media:/usr/src/paperless/media
    - ./export:/usr/src/paperless/export # <--- IMPORTANT
    - ./consume:/usr/src/paperless/consume

The document_exporter command in the script (../export) refers to the path inside the container. Since Paperless often works in /usr/src/paperless/src inside the container, ../export leads to /usr/src/paperless/export, which we have mapped to the outside.

Automation with Cron

Nobody likes doing manual backups. Letโ€™s set up a cronjob that handles this every night at 3 AM.

crontab -e

Add the following line (adjusted to your path):

0 3 * * * /home/deployn/paperless/backup-paperless.sh >> /var/log/paperless_backup.log 2>&1

Or create a different schedule.

Restore: Disaster Recovery

A backup is worthless if you canโ€™t restore it. Here is the way back if your server needs to be completely set up from scratch.

0. Preparation

Initialize a new Paperless directory fresh with Docker Compose, as if it were a new server.

1. Restore Database

Copy the paperless-db.sql from your backup to the server, for example into the ./tmp directory inside the Paperless folder.

Mount the backup into the database container:

paperless-db:
  volumes:
      - ./tmp:/tmp

2. Clean Installation

Start only the database container so that the database is ready.

docker compose up -d paperless-db

3. Import Documents

This is the most important step. We copy the documents from the backup into the export folder of the new installation and let Paperless import them.

  1. Copy the contents of backup/documents/ into the export folder of your new installation.
  2. Start the Paperless webserver container.
  3. Run the importer:
docker compose exec webserver document_importer ../export

The importer reads the manifest.json files, imports the documents back into the system, restores the links in the database, and rebuilds the search index.

Donโ€™t Forget the 3-2-1 Rule

The script above secures data locally to another folder. If your hard drive dies, the backup is gone too.

You should definitely copy the ${BACKUP_BASE_DIR} folder to an external destination. You can do this with:

  • Rclone (Upload to Google Drive, OneDrive, S3)
  • BorgBackup (Encrypted backup to another server)
  • Synology Hyper Backup (If you are on a NAS)

Conclusion

With this script, you have a โ€œset and forgetโ€ solution that handles your storage space extremely efficiently thanks to hard links, but still keeps a full image of your documents and database for every day.

Make sure to test the restore process once in a virtual machine or on a test system before you have to rely on it in an emergency!

Share this post:

This website uses cookies. These are necessary for the functionality of the website. You can find more information in the privacy policy