How to back up and restore Paperless-ngx
Paperless-ngx's own document_exporter writes every original, archived PDF and thumbnail, plus the whole database as manifest.json, into one folder, and document_importer loads that folder into an empty install of the same version. Run it nightly with docker compose exec -T webserver document_exporter ../export, then copy the export folder and your compose files off the server, encrypted. Backing up the media volume plus a database dump works too.
What Paperless-ngx stores
Paperless-ngx is an open-source (GPL-3.0) document manager that runs OCR on your scans and keeps them searchable. This guide assumes the official v3.2.1 Docker Compose files in /opt/paperless. Their .env sets COMPOSE_PROJECT_NAME=paperless, which prefixes the volume names:
docker volume ls --filter name=paperless_| What | Where | Back up? |
|---|---|---|
| Originals, exactly as consumed | paperless_media: documents/originals | Yes. Nothing can recreate them. |
| Archived PDF/A versions with the OCR text | paperless_media: documents/archive | Yes. document_archiver can rebuild them, but runs OCR on every document again. |
| Thumbnails | paperless_media: documents/thumbnails | Optional. document_thumbnails rebuilds them. |
| Database: documents' metadata, tags, users, mail rules, workflows, notes | paperless_data: db.sqlite3, or paperless_pgdata / paperless_dbdata | Yes, as an export or a dump |
| Search index, classifier model, logs | paperless_data: index/, classification_model.pickle, log/ | No. The container checks the index at every start and rebuilds it if needed; the model is retrained on a schedule. |
| Task broker | paperless_redisdata | No |
| Consumption folder | ./consume | No. Files there aren't in Paperless yet. |
docker-compose.yml, docker-compose.env, .env | /opt/paperless | Yes: PAPERLESS_SECRET_KEY, database passwords, settings |
PAPERLESS_SECRET_KEY signs sessions and tokens; a restore under a different value logs everyone out but loses nothing. If you set PAPERLESS_EMPTY_TRASH_DIR, back that folder up too.
Exporter or volumes?
Paperless-ngx's docs describe both. This guide uses the portable one, the exporter, as the main backup:
| Compared | document_exporter | Volumes and a database dump |
|---|---|---|
| What you get | One folder or zip: every file, with readable names, plus manifest.json | The media volume as it is, plus a SQL dump |
| Database engine | Any. The manifest is JSON, so it imports into SQLite, PostgreSQL or MariaDB | The same engine |
| Restores onto | An empty install of the same version | The same version or newer; migrations run at startup |
| Left out | API tokens | Nothing |
| Extra disk | A second copy of your documents in export/ | The dump |
Run the document exporter
The official compose files mount the export folder beside them at /usr/src/paperless/export, and commands run from /usr/src/paperless/src, so the target is ../export. From /opt/paperless:
docker compose exec -T webserver document_exporter ../export --delete --no-progress-bar-Tstopsdocker compose execfrom allocating a terminal; without it, cron runs fail withThe input device is not a TTY.- The first run copies everything. Later runs copy only files whose modification time or size changed, and rewrite
manifest.jsonandmetadata.json.-c(--compare-checksums) compares checksums instead, more slowly. --deleteremoves files that no longer belong to the export, such as deleted documents. Use it only on a folder that holds nothing else.--no-progress-barkeeps the cron log readable.- The exporter holds Paperless's media lock while it runs, so a document being consumed waits instead of landing half in the export.
Other flags worth knowing:
-p(--use-folder-prefix) sorts files intooriginals,archiveandthumbnailsfolders instead of one flat folder.-f(--use-filename-format) names files with yourPAPERLESS_FILENAME_FORMATinstead of[date created] [correspondent] [title].-naand-ntskip archive files and thumbnails; after an import they are missing untildocument_archiveranddocument_thumbnailsrecreate them.-z(--zip) writes one zip named after today's date, such asexport-2026-10-04.zip;-znsets another name. It can't be combined with-cor-cj, and together with--deleteit removes everything else in the folder, older zips included.--passphraseencrypts the mail account passwords and OAuth tokens in the export, otherwise plain text (No passphrase was given, sensitive fields will be in plaintext). It leaves documents unencrypted, and an export made with it can't be imported without it.--data-onlyexports the database without files, for database upgrades.
API tokens are never exported.
Schedule it
The export folder mirrors the current state; it is not a history. Run it nightly and let a backup tool keep dated versions elsewhere:
15 2 * * * root cd /opt/paperless && docker compose exec -T webserver document_exporter ../export --delete --no-progress-bar >> /var/log/paperless-export.log 2>&1Time one run by hand and schedule the off-site copy after it. Check free space first: the export repeats what the media volume holds.
du -sh "$(docker volume inspect --format '{{ .Mountpoint }}' paperless_media)"To keep dated copies on the server instead, export zips and prune them. Each zip is a full copy, written as export-<date>.zip.tmp and renamed when finished:
15 2 * * * root cd /opt/paperless && docker compose exec -T webserver document_exporter ../export --zip --no-progress-bar >> /var/log/paperless-export.log 2>&1
45 4 * * * root find /opt/paperless/export -name 'export-*.zip' -mtime +7 -deleteOr back up the volumes and the database
Dump the database first and copy the media second, so the worst case is a few files the database doesn't know (reported as orphaned), not rows pointing at missing files. With PostgreSQL (the db service):
install -d -m 700 /var/backups/paperlessdocker compose exec -T db pg_dump -U paperless -d paperless | gzip > /var/backups/paperless/paperless-db-$(date +%F).sql.gzWith SQLite, the database is db.sqlite3 in paperless_data, and Paperless-ngx 3 opens it in WAL mode, so recent changes may still sit in db.sqlite3-wal. Copy it with SQLite's backup command from the host (apt install sqlite3), not cp:
sqlite3 "$(docker volume inspect --format '{{ .Mountpoint }}' paperless_data)/db.sqlite3" ".timeout 10000" ".backup '/var/backups/paperless/db.sqlite3'"Then archive paperless_media as in Docker volume backups. MariaDB is in Backing up a database in Docker Compose; SQLite backups explains .backup. Restore onto the same version or newer, then run the sanity checker below.
Encrypt the off-site copy
Paperless-ngx stores documents as plain files, often tax returns, contracts and medical letters, and the export adds readable names, user accounts and, without --passphrase, mailbox passwords. Encrypt before anything leaves the server:
- restic encrypts everything in its repository with AES-256 and only uploads what changed, which suits an export folder that changes a little each night.
- Or pack the folder with tar and encrypt it with gpg, as Encrypting backups shows, then copy the file with rclone.
Keep the decryption key, and any export passphrase, outside Paperless and off the server.
Restore with the document importer
The importer needs an empty Paperless-ngx of the version that made the export. metadata.json in the export records it; for a zip, read it with unzip -p export-2026-10-04.zip metadata.json:
grep version /opt/paperless/export/metadata.json- On the new server, put
docker-compose.yml,docker-compose.envand.envfrom your backup in/opt/paperless, and change the webserver'simage:fromlatestto that version, such asghcr.io/paperless-ngx/paperless-ngx:3.2.1. - Put the export into
/opt/paperless/export: the folder's contents, or the zip. - Leave
PAPERLESS_ADMIN_USERunset and don't create an account in the browser. The importer expects no users and no documents. - Start the stack with
docker compose up -dand wait for the web page to load.
docker compose exec -T webserver document_importer ../exportFor a zip, name the file: ../export/export-2026-10-04.zip; it is unpacked inside the container first, so leave room. The importer checks every file in the manifest, loads the database, copies the files and rebuilds the search index. Add --passphrase if the export has one, then log in with your old account and create new API tokens.
Upgrades, versions and switching databases
- Upgrades don't need the exporter: stop the stack, back up,
docker compose pull,docker compose up; the new image migrates the database as it starts. Read the release notes first. - An import needs matching versions. The docs warn that an export can't be imported into a different version, because it is an exact image of the database and migrations change its layout. The importer only warns about a mismatch and carries on, so pin the image tag on the new server, import, then upgrade.
- Switching database engines is what the exporter is for. Paperless-ngx's docs up to 2.20 called an export followed by an import into a clean installation the best way to migrate between database types: export from SQLite, start an empty install of the same version with
PAPERLESS_DBENGINE=postgresql, import. - A new PostgreSQL or MariaDB major version: follow the database's own upgrade docs, or export and import with
--data-onlyinto a newly created database, without changing any paths. See PostgreSQL major upgrades.
Test a restore
Import the off-site copy, not the live export folder, into a private second instance. A throwaway server is simplest; on the same server, use a separate folder and project name, with room for another copy of every document:
mkdir -p /opt/paperless-restore-test/exportcp /opt/paperless/docker-compose.yml /opt/paperless/docker-compose.env /opt/paperless/.env /opt/paperless-restore-test/Edit the copies before you run anything:
.env:COMPOSE_PROJECT_NAME=paperless-restore-test, so the copy gets its own containers and volumes.docker-compose.yml: pin the image to the export's version, and change the port to"127.0.0.1:8001:8000"so only the server can reach it.docker-compose.env: addPAPERLESS_URL=http://localhost:8001,PAPERLESS_EMAIL_TASK_CRON=disableandPAPERLESS_WORKFLOW_SCHEDULED_TASK_CRON=disable. The last two stop the copy from fetching your real mailboxes, and applying your mail rules to them, and from running scheduled workflows.
Change COMPOSE_PROJECT_NAME before any docker compose command in the test folder. Under the original name, Compose would act on your live containers and volumes, and the cleanup below would delete them.
Put the restored export into /opt/paperless-restore-test/export, then, in that folder, start the copy, import and check:
docker compose up -ddocker compose exec -T webserver document_importer ../exportdocker compose exec -T webserver document_sanity_checkerThe sanity checker compares every original and archive file's checksum with the database and reports missing or unreadable files. Then log in through an SSH tunnel at http://localhost:8001, compare the document count with the live instance and open a few old ones:
ssh -N -L 8001:127.0.0.1:8001 root@<server-ip>Remove the copy afterwards. down -v deletes the containers and volumes of this project only, which is why the project name matters:
docker compose down -vrm -rf /opt/paperless-restore-testTime the import; that is most of your recovery time. Testing restores turns this into a routine.
Common errors
| Error | Fix |
|---|---|
The input device is not a TTY | Add -T after exec in cron jobs and scripts. |
That path doesn't exist | The export target is missing. With the official compose files, use ../export. |
--compare-checksums and --compare-json have no effect when used with --zip | Drop -c and -cj when you export a zip. |
chown: changing ownership of '../export': Operation not permitted | The folder is on a filesystem, such as an NFS share, where the container can't change owners. Use a local folder or allow chown there. |
That directory doesn't appear to contain a manifest.json file. | Point the importer at the folder that holds manifest.json, or at the zip. |
No passphrase was given, but this export contains encrypted fields | Add --passphrase with the one used for the export. |
Version mismatch: Currently 3.2.1, importing 3.1.3. Continuing, but import may fail. | Start over on the export's version, import, then upgrade. |
Found existing user(s), this might indicate a non-empty installation | An account exists already, from the browser or PAPERLESS_ADMIN_USER. Start again with empty volumes. |
Frequently asked questions
- Where does Paperless-ngx store my documents?
- In the media volume, under documents/originals, with archived PDF/A copies in documents/archive and thumbnails in documents/thumbnails. The database and search index are in the data volume, or in PostgreSQL or MariaDB if you use one.
- Is document_exporter a full backup?
- Nearly: it exports every document file and the whole database, including users, mail accounts and settings. API tokens are left out, and the export only imports into an empty install of the same version.
- Can I import a Paperless-ngx export into a newer version?
- The docs say to match versions. Install the version recorded in the export's metadata.json, import, then upgrade normally.
- Do I need to back up the search index?
- No. The Docker image checks the index at every start and rebuilds it when needed, and document_index reindex rebuilds it by hand.
- How do I move Paperless-ngx from SQLite to PostgreSQL?
- Export with document_exporter, start an empty install of the same version with PAPERLESS_DBENGINE=postgresql, and import with document_importer.
How this was checked
Commands, limits and prices were checked against these official pages, on October 4, 2026:
- Paperless-ngx docs (v3.2.1 source): Administration
- Paperless-ngx docs (v3.2.1 source): Setup
- Paperless-ngx docs (v3.2.1 source): Configuration
- Paperless-ngx docs (v3.2.1 source): Troubleshooting
- Paperless-ngx docs (v3.2.1 source): FAQ
- Paperless-ngx docs (v3.2.1 source): v3 migration guide
- Paperless-ngx docs (v2.20.0 source): moving between database types
- Paperless-ngx v3.2.1 Docker Compose files
- Paperless-ngx source v3.2.1: document_exporter
- Paperless-ngx source v3.2.1: document_importer
- Paperless-ngx source v3.2.1: export passphrase fields
- Paperless-ngx source v3.2.1: export sinks (zip, --delete)
- Paperless-ngx source v3.2.1: settings (paths)
- Paperless-ngx source v3.2.1: settings (SQLite WAL mode)
- Paperless-ngx source v3.2.1: consumer (media lock)
- Paperless-ngx source v3.2.1: Docker wrapper for document_exporter
- restic documentation: References (repository encryption)
- PostgreSQL documentation: pg_dump
- SQLite: Command Line Shell (.backup)
- Docker documentation: docker compose exec
- Docker documentation: docker compose down
- Docker documentation: docker volume ls