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.
Table of Contents
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:
- Database Consistency: If you copy the database files (PostgreSQL) while the container is running, the backup may be corrupt and unusable.
- 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:
- Create a clean dump of the PostgreSQL database.
- Convert the documents into a processable format using the Paperless Exporter (this also writes metadata into
jsonfiles). - 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).
- 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.
- Copy the contents of
backup/documents/into theexportfolder of your new installation. - Start the Paperless webserver container.
- 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!