Skip to content

pless

A searchable archive of your documents, on hardware you own.

Paperless-ngx is a document management system for the paper that accumulates in a life: invoices, receipts, contracts, diplomas, letters from institutions that still send letters. You feed it scans and PDFs; it runs OCR over them so the text inside becomes searchable, then sorts them by correspondent, document type and date — guessing sensibly, and learning from your corrections. What you get back is a filing cabinet you can grep, reachable from a browser or a phone.

It is open source and self-hosted, which is the point: your tax returns and your children's birth certificates stay on hardware you control rather than in someone else's product roadmap.

pless sets that up and operates it — on a Raspberry Pi in your home, a cloud server, or a local VM — with two properties most self-hosting guides skip:

  • Encrypted at rest. Your documents live on a LUKS2-encrypted volume. The key is never stored on the machine. Someone who walks off with your Pi gets hardware and an operating system, not your tax returns.
  • Invisible on your network. After hardening, nothing listens on your LAN. Paperless is bound to localhost, the database publishes no ports at all, and SSH accepts connections only over your Tailscale network. A compromised device on your Wi-Fi finds nothing to attack.

Alpha — read this before you rely on it

pless is under active development. Import, backup, restore and the restore rehearsal all work and are covered by tests, but the full flow has not been drilled on real Raspberry Pi hardware (#4) and the Hetzner target has never been run against the live API (#5).

Several real bugs in this project were invisible to tests and obvious the moment the code ran on a machine. Keep your originals until pless backup status shows a successful run that includes them — pless preflight will tell you the same thing.

What it looks like

$ pless doctor
✓ Python 3.12.3 (requires 3.12+)
✓ ssh found in PATH
✓ pless.toml found
✓ SSH key found (~/.ssh/id_ed25519)

Host
✓ [host] points at archive.local
✓ SSH key found (~/.ssh/id_ed25519)
✓ [storage] data_mode=file (100 GB LUKS file)

All clear. Next: pless bootstrap
$ pless audit
✓ listening sockets: Nothing listens outside loopback — your LAN sees zero ports.
✓ firewall: UFW active, default deny, no LAN-open rules.
✓ docker ports: No container publishes outside loopback.
✓ ssh: Key-based authentication only.
✓ encrypted storage: Data lives on the LUKS device /dev/mapper/paperless-data.

The whole stack

Everything below the dashed line runs on your machine. Nothing in it is reachable from your local network once hardening is applied.

flowchart TB
    subgraph laptop["Your laptop"]
        cli["<b>pless</b><br/><small>Python 3.12 · Typer · Rich<br/>Pydantic · httpx</small>"]
        docs["Your documents<br/><small>PDF · images · Office</small>"]
    end

    subgraph tailnet["Tailscale tailnet — WireGuard"]
        ts["MagicDNS + ACLs"]
    end

    subgraph host["Target: Raspberry Pi · Hetzner · local VM"]
        subgraph os["Debian 13 or Ubuntu 24.04 · arm64 or x86_64"]
            sec["<b>Hardening</b><br/><small>UFW · fail2ban · OpenSSH keys only<br/>unattended-upgrades</small>"]
            luks["<b>LUKS2 volume</b><br/><small>AES-XTS or Adiantum · ext4<br/>key never stored on device</small>"]
            subgraph compose["Docker Compose — bound to 127.0.0.1"]
                web["paperless-ngx<br/><small>Django · OCRmyPDF · Tesseract</small>"]
                pg[("PostgreSQL 16")]
                redis[("Redis 7")]
                tika["Apache Tika"]
                got["Gotenberg"]
            end
        end
    end

    subgraph offsite["Off-site backup"]
        restic["restic<br/><small>encrypted · incremental</small>"]
        b2[("Backblaze B2<br/><small>Object Lock · governance</small>")]
    end

    cli -- "SSH · provisioning and day-2 ops" --> host
    docs -- "REST API upload over Tailscale" --> web
    cli --- ts
    ts --- host
    web --- pg
    web --- redis
    web --- tika
    web --- got
    compose --- luks
    luks -- "nightly snapshot · systemd timer" --> restic
    restic --> b2

Why not just run Docker Compose yourself?

You can, and many people do. pless exists because the interesting parts are not the docker compose up — they are everything around it: making the disk encrypted without storing the key on the box, making sure a reboot doesn't silently start the stack against an unlocked volume, keeping the firewall honest when Docker writes its own iptables rules behind your back, and having a single command that tells you whether any of that has quietly stopped being true.

Where to go next

  • Getting started

    What you need, what it costs, and a ten-minute overview of the whole flow.

  • Installation

    Step by step, from an unboxed Raspberry Pi to a running archive.

  • Maintenance

    Day-to-day operation: updates, health checks, logs, disk usage.

  • Cookbook

    Recipes for the situations you will actually run into.

Status

pless is verified end to end on Debian 13 and Ubuntu 24.04. Here is the honest state of each piece:

Capability Status
Provisioning (Docker, firewall, fail2ban, auto-updates) Working
Encrypted storage with LUKS2 Working
Deploying the Paperless-ngx stack Working
Tailscale access and LAN hardening Working
Exposure auditing Working
Readiness gate (pless preflight) Working
Importing your documents (pless docs upload) Working
Off-site backup with restic Working
Restore, and the restore rehearsal on a fresh machine Working
Bucket provisioning with Object Lock (pless b2 provision) Working
Raspberry Pi as a target Documented, awaiting hardware validation
Hetzner Cloud as a target Code and unit tests, not validated live
Alternative unlock methods Not built
Secrets from a vault rather than .env Not built
Web setup wizard Not built

What comes next

The archive and its backup are both built. What is left is proof on real hardware: the Pi flow is documented and tested but has never run on a Pi (#4), and the Hetzner target has never met the live API (#5). Given this project's history — a package name that differs between distributions, an apt lock held by cloud-init, a service listening on an interface nobody considered — that gap matters more than any remaining feature.

After that: unlocking without typing the passphrase every time (#6), resolving secrets from a vault rather than .env (#7), and a web setup wizard (#8) — which is why the core modules are kept free of typer and rich.

What is being worked on, and the reasoning behind each piece, lives in the issue tracker. This page says what works today; the tracker says what is being done about the rest.