← ~/content

Install Paperless-ngx on Proxmox LXC (2026)

Tutorial~22 min read
Install Paperless-ngx on Proxmox LXC (2026)
intermediate~30 min

Prerequisites

  • Proxmox VE 9 host

Tools

  • SSH terminal
  • Web browser
  • nano

Software

  • debian13
  • python3.13
  • postgresql17
  • proxmox-ve9
  • paperless-ngx2.20.14
Watch on YouTube

Paperless-ngx turns every scrap of paper in your house into a searchable online archive. Scan a document, drop the PDF in a folder, and a few seconds later it is OCR'd, tagged, and indexed. You can search for "electric bill 2023" and the right PDF comes back.

This guide installs Paperless-ngx bare-metal inside an unprivileged Debian 13 LXC container on Proxmox VE 9. No Docker, no helper scripts. We use PostgreSQL for the database, Redis for the task queue, and systemd for process supervision.

Why not the popular Proxmox community helper script? It works fine — the maintainers patch Debian breakage fast, and the current version installs cleanly on Debian 13. We do bare-metal anyway: when something breaks two upgrades from now, you want an install you wrote and understand, not someone else's bash you have to reverse-engineer. Bare-metal also makes it easy to swap components later — a different Python version, a different database, custom OCR languages. The end result is the same shape: a dedicated LXC you can back up, snapshot, and monitor.

What You Will End Up With

  • An unprivileged Debian 13 LXC running Paperless-ngx 2.20.14
  • PostgreSQL 17 and Redis running alongside it in the same container
  • Four systemd services managing the webserver, document consumer, task queue, and scheduler
  • Paperless reachable on the LXC at http://<your-CT-IP>:8000

This guide also includes a short bonus section at the end covering our standard homelab wiring — a Pi-hole local DNS record, a Caddy reverse proxy with valid HTTPS, a PBS backup job, and an Uptime Kuma monitor. Skip the bonus if you do not run those services; the core install is fully functional on its own.

For this guide the Paperless LXC takes CT ID 103 and IP 10.1.20.103. Substitute your own.

NOTE

About 25 to 30 minutes of hands-on time. Most of it is waiting for pip install and apt install to finish.

Step 1: Create the Paperless LXC Container

We create an unprivileged Debian 13 container with nesting=1 and keyctl=1. Nesting is required so systemd inside the container can manage its own cgroups. Keyctl is needed for services that use the kernel keyring — which includes any modern systemd unit.

On the Proxmox host, confirm the Debian 13 template is already downloaded:

Check the Debian 13 template is cached
pveam list local

You should see debian-13-standard_13.1-2_amd64.tar.zst. If it is missing, run pveam update && pveam download local debian-13-standard_13.1-2_amd64.tar.zst first.

Now create the container.

Create the Paperless LXC
pct create 103 local:vztmpl/debian-13-standard_13.1-2_amd64.tar.zst \
    --hostname paperless \
    --cores 2 \
    --memory 4096 \
    --swap 1024 \
    --rootfs local-lvm:20 \
    --net0 name=eth0,bridge=vmbr0,ip=10.1.20.103/24,gw=10.1.20.1 \
    --nameserver 10.1.20.100 \
    --unprivileged 1 \
    --features nesting=1,keyctl=1 \
    --onboot 1 \
    --password
FlagWhat it does
103 (positional)Container VMID. Replace with the next free ID on your Proxmox host. The studio convention is LXCs in 100-199.
local:vztmpl/debian-13-standard_13.1-2_amd64.tar.zstOS template path. Keep — every tutorial in this series uses Debian 13.
--hostname paperlessContainer hostname. Keep or rename to taste — nothing else in this guide depends on the name.
--cores 2CPU cores. Keep — sized for comfortable OCR bursts on a family-scale install.
--memory 4096RAM in MB. Keep.
--swap 1024Swap in MB. Keep — safety net for OCR memory spikes.
--rootfs local-lvm:20Root filesystem at 20 GB on the local-lvm thinpool. Replace local-lvm if your storage pool is named differently (for example local-zfs).
--net0 name=eth0,bridge=vmbr0,ip=10.1.20.103/24,gw=10.1.20.1Network attachment. Replace bridge, ip, and gw with your environment's values.
--nameserver 10.1.20.100DNS server. Replace with your local DNS resolver (Pi-hole if you run one, otherwise your gateway or any public resolver).
--unprivileged 1Unprivileged container. Keep — security default.
--features nesting=1,keyctl=1Enables systemd cgroup management (nesting) and the kernel keyring (keyctl). Keep — Paperless's systemd services need both.
--onboot 1Auto-start on host boot. Keep.
--passwordPrompts for a root password for the container during creation. Keep.

You will be prompted for a root password for the container. Set something memorable — you will need it if you ever console into the CT.

The resource choices are sized for a real homelab user with a family's worth of documents. 2 cores handles OCR bursts comfortably, 4 GB of RAM leaves headroom without being wasteful, and 20 GB of disk fits the install plus years of typical document accumulation. You can grow any of these later.

Start the container:

Start the container
pct start 103

Step 2: Enter the Container, Fix Locales, and Update

From here on, everything happens inside the container unless a step says otherwise.

On the Proxmox host, drop into a shell inside CT 103:

Enter the container shell
pct enter 103

Your prompt should now say root@paperless. Refresh the package list.

Refresh package lists
apt update

The minimal Debian 13 LXC template ships without any locales generated, but the shell inherits LANG=en_US.UTF-8 from the host. The mismatch causes every later command (apt, perl, python, dpkg triggers) to print noisy Setting locale failed warnings. Three quick commands generate en_US.UTF-8 once and the rest of the install runs cleanly.

Install the locales package
apt install -y locales
Enable en_US.UTF-8 in /etc/locale.gen
sed -i 's/^# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen
Generate the locale
locale-gen

Now upgrade any packages that have moved since the template was built.

Apply pending updates
apt upgrade -y

Set the LXC's system timezone. This is what journalctl timestamps, date, and anything reading /etc/localtime use — useful when you're debugging the container later. (Paperless has its own internal timezone setting we configure in Step 9. The two are independent: Paperless ignores the system TZ, and the system TZ doesn't affect Paperless's UI. You want both correct.)

Set the container timezone
timedatectl set-timezone America/New_York

Substitute your own zone — timedatectl list-timezones will print the full list.

Step 3: Install System Dependencies

Paperless-ngx is a Python web app, but the OCR pipeline leans on a pile of system libraries: Tesseract for text extraction, Ghostscript for PDF manipulation, ImageMagick for image conversion, and so on. We also need curl for downloading the release tarball — the minimal Debian 13 template does not include it.

NOTE

Older Paperless docs include liblept5 in the dependency list. Leptonica was bumped to version 6 upstream, and Debian 13 ships only the new libleptonica6 package. We omit it from our apt install line entirely — tesseract-ocr pulls in libleptonica6 as a transitive dependency, so the OCR pipeline gets what it needs without us specifying anything.

Install the Python build tools, PostgreSQL client libraries, ImageMagick, file-handling utilities, and curl.

Install Python and system libraries
apt install -y python3-pip python3-dev python3-venv \
    imagemagick fonts-liberation libpq-dev \
    default-libmysqlclient-dev pkg-config libmagic-dev \
    poppler-utils curl

Install the OCR toolchain. (ghostscript is omitted here because imagemagick already pulled it in transitively above.) The base tesseract-ocr package ships English language data automatically, so no separate language pack is needed for English.

Install the OCR toolchain
apt install -y unpaper icc-profiles-free qpdf pngquant tesseract-ocr

TIP

If you scan documents in other languages, install the matching language packs after the base install (e.g. apt install -y tesseract-ocr-deu tesseract-ocr-fra tesseract-ocr-spa) and update PAPERLESS_OCR_LANGUAGE in /etc/paperless.conf to a +-separated list like eng+deu+fra. Run apt-cache search tesseract-ocr- to see every available pack.

NOTE

Some older Paperless guides include a step to edit /etc/ImageMagick-6/policy.xml to allow PDF processing. On Debian 13 that file does not exist (the package is now ImageMagick 7), and the default policy already allows PDF — no edit required.

Step 4: Install and Configure PostgreSQL

Debian 13 ships PostgreSQL 17 in the default repos. Install it.

Install PostgreSQL 17
apt install -y postgresql

The package starts PostgreSQL automatically. Confirm it is running:

Confirm PostgreSQL is running
systemctl status postgresql

Now create a database and a user for Paperless. We run those commands as the postgres system user using runuser. (runuser is the right tool when you are already root and just need to drop into another user — no sudo install required.)

First, generate a random password for the database user. Copy this value to a safe place — you will paste it into paperless.conf in a moment.

Generate a random database password
openssl rand -base64 24

Create the paperless database user. It will prompt you for the password — paste the value you just generated when asked.

Create the paperless database user
runuser -u postgres -- createuser --pwprompt paperless

Create the paperless database, owned by the paperless user. We pin UTF-8 encoding explicitly. On a fresh Debian 13 LXC, the PostgreSQL cluster initializes itself with SQL_ASCII encoding because apt install postgresql runs pg_createcluster before any locale variables are exported in the install shell. SQL_ASCII silently accepts bytes but rejects unicode escape sequences on insert — which means the moment Paperless tries to store OCR'd text containing any non-ASCII character (or even a smart quote in a filename), it dies with unsupported Unicode escape sequence. We sidestep the cluster's broken default by creating the database from template0 with explicit UTF-8 encoding and the always-available C.UTF-8 locale.

Create the paperless database
runuser -u postgres -- createdb --owner=paperless --encoding=UTF8 --locale=C.UTF-8 --template=template0 paperless
FlagWhat it does
--owner=paperlessThe paperless user owns the new database. Keep.
--encoding=UTF8Forces UTF-8 encoding regardless of cluster default. Keep — this is the bug fix.
--locale=C.UTF-8UTF-8-aware C locale. Always available on glibc-based systems (no locale-gen required). Keep.
--template=template0Required when overriding encoding/locale — template1 inherits the cluster's broken default and would block the override. Keep.

Verify the connection works. When prompted, enter the password you set above.

Verify the database connection
psql -h localhost -U paperless -d paperless -c '\conninfo'

You should see something like You are connected to database "paperless" as user "paperless" on host "localhost".

Step 5: Install Redis

Redis is the message broker for Celery, which runs Paperless's background tasks (OCR, consumer, scheduled jobs).

Install Redis
apt install -y redis-server

Redis starts automatically on install. Confirm:

Confirm Redis is running
systemctl status redis-server

Test it responds:

Test Redis responds
redis-cli ping

You should see PONG.

Step 6: Create the Paperless System User

Paperless runs as a dedicated system user with its home directory at /opt/paperless. The --system flag gives it a UID below 1000 and no login shell by default.

Create the paperless system user
adduser paperless --system --home /opt/paperless --group
FlagWhat it does
paperless (positional)Username. Keep — the systemd units and file ownership throughout the guide all reference this name.
--systemCreates a system user (UID below 1000, no login shell by default). Keep.
--home /opt/paperlessSets the home directory. Keep — every later step uses this path.
--groupAlso creates a matching paperless group. Keep.

Step 7: Download and Extract Paperless-ngx

We pin a specific version rather than tracking main. Pinning makes the install reproducible and gives you control over when to upgrade.

Set a version variable at the top of the shell so later commands can use it:

Pin the Paperless version
export PAPERLESS_VERSION=2.20.14

TIP

To find the latest stable version, open github.com/paperless-ngx/paperless-ngx/releases in a browser and read the top entry. Do not take a pre-release unless you know what you are doing.

Move into the paperless home directory so downloads and extraction land there.

Move into the paperless home directory
cd /opt/paperless

Download the release tarball. We run curl as the paperless user so the downloaded file ends up owned correctly.

Download the release tarball
runuser -u paperless -- curl -L -O \
    https://github.com/paperless-ngx/paperless-ngx/releases/download/v${PAPERLESS_VERSION}/paperless-ngx-v${PAPERLESS_VERSION}.tar.xz

Extract the archive. It creates a paperless-ngx/ subdirectory with all the source, config, and scripts inside.

Extract the tarball
runuser -u paperless -- tar -xf paperless-ngx-v${PAPERLESS_VERSION}.tar.xz

Move the extracted contents up one level so the source lives directly at /opt/paperless/src, /opt/paperless/requirements.txt, and so on. The shopt -s dotglob pulls dotfiles too.

Flatten the extracted directory
runuser -u paperless -- bash -c "shopt -s dotglob && mv paperless-ngx/* . && rmdir paperless-ngx"

Clean up the tarball:

Delete the tarball
rm paperless-ngx-v${PAPERLESS_VERSION}.tar.xz

Step 8: Create a Python Virtual Environment and Install Dependencies

We build an isolated Python 3.13 environment for Paperless so its dependencies do not collide with system packages.

Create the virtual environment at /opt/paperless/.venv:

Create the Python virtual environment
runuser -u paperless -- python3 -m venv /opt/paperless/.venv

Upgrade pip inside the venv so we get recent dependency resolution behavior.

Upgrade pip inside the venv
runuser -u paperless -- /opt/paperless/.venv/bin/pip install --upgrade pip

Install the Python dependencies from the requirements.txt shipped with the release. This takes a few minutes — it compiles some native extensions.

Install Paperless Python dependencies
runuser -u paperless -- /opt/paperless/.venv/bin/pip install -r /opt/paperless/requirements.txt

If this step fails with a compilation error, the most common cause is a missing system dev package. Re-read the error and install whatever library it names via apt install -y <name>-dev.

Step 9: Configure Paperless

Paperless reads its configuration from /etc/paperless.conf. We will write that file from scratch with just the settings our install needs. (The release tarball ships an example at /opt/paperless/paperless.conf if you want to browse every available setting later — it's a wall of commented-out defaults.)

First generate a Django secret key. This is what Django uses to sign session cookies — treat it like a password.

Generate the Django secret key
openssl rand -base64 64 | tr -d '\n'; echo

The tr -d '\n'; echo part strips base64's 76-char line wrap so the key prints on a single line — Paperless reads paperless.conf as KEY=value lines, and a wrapped value would silently break things. Copy the output somewhere safe, just like you did with the database password. Now open the config for editing.

Open the config for editing
nano /etc/paperless.conf

Paste the following, then edit the values marked REPLACE below:

  • REPLACE_ME_SECRET_KEY → your generated secret key
  • REPLACE_ME_DB_PASSWORD → your PostgreSQL password
  • 10.1.20.103 (in PAPERLESS_URL and PAPERLESS_ALLOWED_HOSTS) → your CT's IP address
  • America/New_York → your timezone, if different
/etc/paperless.conf
# Required services
PAPERLESS_REDIS=redis://localhost:6379
PAPERLESS_DBENGINE=postgresql
PAPERLESS_DBHOST=localhost
PAPERLESS_DBPORT=5432
PAPERLESS_DBNAME=paperless
PAPERLESS_DBUSER=paperless
PAPERLESS_DBPASS=REPLACE_ME_DB_PASSWORD
 
# Security
PAPERLESS_SECRET_KEY=REPLACE_ME_SECRET_KEY
PAPERLESS_URL=http://10.1.20.103:8000
PAPERLESS_ALLOWED_HOSTS=10.1.20.103,localhost
 
# OCR
PAPERLESS_OCR_LANGUAGE=eng
PAPERLESS_OCR_MODE=skip
 
# Consumer
PAPERLESS_CONSUMER_RECURSIVE=true
PAPERLESS_CONSUMER_POLLING=10
 
# Workers
PAPERLESS_TASK_WORKERS=2
PAPERLESS_THREADS_PER_WORKER=1
 
# Timezone
PAPERLESS_TIME_ZONE=America/New_York

Save with Ctrl+X, Y, Enter.

NOTE

If you complete the bonus Caddy step at the end of this guide, you will come back and update PAPERLESS_URL and PAPERLESS_ALLOWED_HOSTS to add the public hostname. For now the IP-only values are correct for the core install.

Lock down the file so only root and the paperless user can read the password.

Lock down ownership
chown root:paperless /etc/paperless.conf
Restrict read access to root and paperless
chmod 640 /etc/paperless.conf

Step 10: Create the Runtime Directories

Paperless needs three writable directories at runtime — the consume folder it watches for new uploads, the media folder where stored documents live, and the data folder that holds the Whoosh search index. The migrate step in the next section refuses to run if these do not exist, so we create them now.

Create the runtime directories
mkdir -p /opt/paperless/consume /opt/paperless/media /opt/paperless/data

Set ownership so the paperless user can write to them.

Set ownership on the runtime directories
chown -R paperless:paperless /opt/paperless/consume /opt/paperless/media /opt/paperless/data

Step 11: Initialize the Database and Create an Admin User

Before we can run Django's migration command, we need to load the config file we just wrote into the current shell. Without this, manage.py would read no PAPERLESS_DB* env vars, fall back to the SQLite default, and create the schema in the wrong database — only to have the systemd services in Step 12 connect to the (still empty) PostgreSQL and 500 on every request. The systemd EnvironmentFile= directive only applies to services launched by systemd, not to one-shot commands we run by hand.

Source paperless.conf into the current shell
set -a; source /etc/paperless.conf; set +a

set -a tells bash to export every variable it sees from this point forward; sourcing the conf brings in PAPERLESS_DBHOST, PAPERLESS_DBPASS, etc; set +a restores normal behavior. Run env | grep PAPERLESS_DB if you want to confirm the values are present.

Move into the source directory:

Enter the source directory
cd /opt/paperless/src

Run the migration. The --preserve-environment flag tells runuser to carry the env vars we just loaded into the paperless user's shell, instead of starting it with a clean environment.

Apply database migrations
runuser -u paperless --preserve-environment -- /opt/paperless/.venv/bin/python3 manage.py migrate

You should see a long stream of Applying ... OK lines ending with Applying auditlog.xxxx... OK. If you instead see migrations applying to a SQLite file under /opt/paperless/data/, the env vars were not loaded — go back, re-run the source line, and try again.

Create the admin user you will log in with. The script prompts for a username, email, and password.

Create the admin login
runuser -u paperless --preserve-environment -- /opt/paperless/.venv/bin/python3 manage.py createsuperuser

Step 12: Install Systemd Services

Paperless runs as four separate systemd services: the webserver, the document consumer (watches the consume directory), the task queue (Celery workers that do OCR), and the scheduler (Celery beat for periodic tasks like email checking).

The release tarball ships example unit files. Each of ours has two pieces tailored to this install: an EnvironmentFile=/etc/paperless.conf line so the service inherits the config we wrote in Step 9 (without it, Paperless silently falls back to defaults — wrong database, empty CSRF allow-list, default secret key), and ExecStart lines that use our venv's python3 and celery instead of the system ones.

Create the webserver unit:

Create the webserver unit
nano /etc/systemd/system/paperless-webserver.service

Paste the following, then save with Ctrl+X, Y, Enter.

/etc/systemd/system/paperless-webserver.service
[Unit]
Description=Paperless webserver
After=network.target postgresql.service redis-server.service
Wants=network.target
Requires=redis-server.service postgresql.service
 
[Service]
EnvironmentFile=/etc/paperless.conf
User=paperless
Group=paperless
WorkingDirectory=/opt/paperless/src
Environment=GRANIAN_HOST=0.0.0.0
Environment=GRANIAN_PORT=8000
Environment=GRANIAN_WORKERS=2
ExecStart=/opt/paperless/.venv/bin/granian --interface asginl --ws paperless.asgi:application
Restart=on-failure
 
[Install]
WantedBy=multi-user.target

Create the document consumer unit:

Create the consumer unit
nano /etc/systemd/system/paperless-consumer.service

Paste, then save with Ctrl+X, Y, Enter.

/etc/systemd/system/paperless-consumer.service
[Unit]
Description=Paperless consumer
Requires=redis-server.service postgresql.service
After=redis-server.service postgresql.service
 
[Service]
EnvironmentFile=/etc/paperless.conf
User=paperless
Group=paperless
WorkingDirectory=/opt/paperless/src
ExecStart=/opt/paperless/.venv/bin/python3 manage.py document_consumer
Restart=on-failure
 
[Install]
WantedBy=multi-user.target

Create the task queue unit (Celery workers):

Create the task queue unit
nano /etc/systemd/system/paperless-task-queue.service

Paste, then save with Ctrl+X, Y, Enter.

/etc/systemd/system/paperless-task-queue.service
[Unit]
Description=Paperless Celery Workers
Requires=redis-server.service postgresql.service
After=redis-server.service postgresql.service
 
[Service]
EnvironmentFile=/etc/paperless.conf
User=paperless
Group=paperless
WorkingDirectory=/opt/paperless/src
ExecStart=/opt/paperless/.venv/bin/celery --app paperless worker --loglevel INFO
Restart=on-failure
 
[Install]
WantedBy=multi-user.target

Create the scheduler unit (Celery beat):

Create the scheduler unit
nano /etc/systemd/system/paperless-scheduler.service

Paste, then save with Ctrl+X, Y, Enter.

/etc/systemd/system/paperless-scheduler.service
[Unit]
Description=Paperless Celery Beat
Requires=redis-server.service postgresql.service
After=redis-server.service postgresql.service
 
[Service]
EnvironmentFile=/etc/paperless.conf
User=paperless
Group=paperless
WorkingDirectory=/opt/paperless/src
ExecStart=/opt/paperless/.venv/bin/celery --app paperless beat --loglevel INFO
Restart=on-failure
 
[Install]
WantedBy=multi-user.target

Reload systemd so it sees the new units:

Reload systemd
systemctl daemon-reload

Enable and start all four at once:

Enable and start all four services
systemctl enable --now paperless-webserver paperless-consumer paperless-task-queue paperless-scheduler

Step 13: Verify Paperless is Running

Check the status of each service:

Check each service is running
systemctl status paperless-webserver paperless-consumer paperless-task-queue paperless-scheduler --no-pager

All four should show Active: active (running). If any say failed, check its logs with journalctl -u <unit-name> --no-pager -n 50.

Test the webserver responds on localhost:

Test the webserver responds
curl -I http://localhost:8000/accounts/login/

You should see HTTP/1.1 200 OK.

Step 14: Upload Your First Document

Open Paperless in a browser at http://10.1.20.103:8000 (substitute your CT's IP). Log in with the superuser credentials you created in Step 11.

Click the + Add Document button in the upper right, or drag a PDF file onto the browser window. Pick anything — an old bill, a warranty card, a PDF manual.

The document appears in the dashboard with a spinning indicator. Behind the scenes, the Celery task queue is running Tesseract OCR on every page and storing the resulting text in PostgreSQL. For a typical 2-page bill this takes 5-15 seconds on the resources we allocated.

When OCR completes, click the document. You will see the extracted text on the right side, and you can now search for any phrase in the document via the search bar at the top.

If that works: congratulations, you have a fully wired Paperless-ngx install.


Standard Homelab Wiring (Optional Bonus)

The remaining steps wire Paperless into the homelab stack we use across this series — local DNS, real HTTPS via Caddy, scheduled backups to PBS, and uptime monitoring. Skip this section if you do not run those services — Paperless is fully usable at http://10.1.20.103:8000 as it stands.

In our setup the supporting services live at:

ServiceIPHostname
Proxmox host10.1.20.10pve.hake.rodeo
Pi-hole10.1.20.100pihole.hake.rodeo
Caddy10.1.20.101(proxies the others)
Uptime Kuma10.1.20.102uptime.hake.rodeo
PBS10.1.20.200pbs.hake.rodeo

Substitute your own IPs and domain throughout the bonus steps.

Pi-hole Local DNS Record

We want paperless.hake.rodeo to resolve to the Caddy LXC so Caddy can terminate TLS and proxy to the Paperless backend.

Open your Pi-hole admin UI in a browser, log in, then click Local DNS Records in the left sidebar.

In the form at the top:

  • Domain: paperless.hake.rodeo
  • IP Address: 10.1.20.101 (your Caddy LXC)

Click Add. The record appears in the list below. No restart is required — Pi-hole picks up the change immediately.

Verify resolution from inside your network:

Verify Pi-hole is serving the record
dig +short paperless.hake.rodeo @10.1.20.100

You should see 10.1.20.101.

Update Paperless's Allowed Hosts

We update Paperless's config before wiring up Caddy so that the moment the proxy goes live, Paperless is already willing to accept requests for the public hostname. Django will reject any request whose Host header is not in ALLOWED_HOSTS, and reject any login POST whose Origin is not in CSRF_TRUSTED_ORIGINS — both of which Paperless derives from PAPERLESS_URL.

Inside the Paperless container (re-enter with pct enter 103 if you have exited it), open the config:

Open paperless.conf
nano /etc/paperless.conf

Change the two security lines to the public hostname:

/etc/paperless.conf (edit these two lines)
PAPERLESS_URL=https://paperless.hake.rodeo
PAPERLESS_ALLOWED_HOSTS=paperless.hake.rodeo,10.1.20.103,localhost

Save with Ctrl+X, Y, Enter, then restart the webserver so it picks up the new config:

Restart the webserver
systemctl restart paperless-webserver

Paperless is now ready for paperless.hake.rodeo even though no requests will arrive under that name yet. Caddy is next.

Caddy Reverse Proxy Block

Exit the Paperless container, then on the Proxmox host enter the Caddy container:

Exit the Paperless container
exit
Enter the Caddy container
pct enter 101

Open the Caddyfile:

Open the Caddyfile
nano /etc/caddy/Caddyfile

Add the following block at the end of the file. The Cloudflare API token is already loaded from /etc/caddy/caddy.env for the other site blocks, so the same reference works here.

/etc/caddy/Caddyfile (append)
paperless.hake.rodeo {
    tls {
        dns cloudflare {env.CLOUDFLARE_API_TOKEN}
    }
    reverse_proxy 10.1.20.103:8000
}

Save with Ctrl+X, Y, Enter, then reload Caddy. Reload is graceful — it does not drop existing connections.

Reload Caddy
systemctl reload caddy

Open https://paperless.hake.rodeo in a browser. You should see the Paperless login page with a valid HTTPS certificate, and login should succeed because we configured the trusted hostname before bringing the proxy online.

PBS Backup Job

If you already have a Proxmox Backup Server job running on this host, just fold Paperless into it. In the PVE web UI go to Datacenter → Backup, double-click the existing job, and tick 103 (paperless) in the VM selection list. Save. Optionally select the job and click Run Now to immediately back up the new container without waiting for the next scheduled run.

If you do not have a PBS job set up yet, follow our Proxmox Backup Server setup guide first — that gets you a working backup target, retention policy, and verify schedule, all of which Paperless can ride on the moment it exists.

Uptime Kuma Monitors

We add two monitors so that when something goes wrong, the dashboard tells us what is wrong. A ping monitor against the LXC IP catches the container being down (host reboot, OOM, networking gone). An HTTP monitor against the public URL catches Paperless itself crashing while the LXC is still up. If both go red together the LXC is gone; if only HTTP goes red, the container is fine and the service died.

Open Uptime Kuma and click + Add New Monitor. Configure the first as a ping to the LXC:

  • Monitor Type: Ping
  • Friendly Name: Paperless LXC
  • Hostname: 10.1.20.103
  • Heartbeat Interval: 60 seconds
  • Retries: 3

Click Save, then click + Add New Monitor again for the service-level check:

  • Monitor Type: HTTP(s)
  • Friendly Name: Paperless service
  • URL: https://paperless.hake.rodeo/accounts/login/
  • Heartbeat Interval: 60 seconds
  • Retries: 3

Expand Advanced and check Accept Status Codes 200-299. Paperless returns 200 on the login page even when unauthenticated, which is the cheapest health signal we can hit without setting up authentication.

Click Save. Both monitors appear in your dashboard — green blocks mean healthy.

Next Steps

Now that you have Paperless running you can explore the features it ships with:

  • Tags and correspondents. Train Paperless on your filing conventions by creating tags and correspondents, then auto-apply them via matching rules under Settings → Automatic matching.
  • Email intake. Paperless can watch an IMAP inbox and ingest attachments automatically. That is a separate tutorial.
  • Mobile upload. The iOS and Android apps scan documents with your phone camera and upload straight to the consume folder. Both are open source on the Paperless-ngx GitHub organization.
  • AI-assisted tagging and OCR. Paperless-GPT is a companion service that talks to your Paperless API and uses an LLM (OpenAI, Ollama, or any compatible endpoint) to auto-generate titles, suggest tags and correspondents, and run vision-LLM OCR — much better than Tesseract on handwriting or phone-camera scans. Worth setting up once your collection is large enough that hand-tagging gets tedious. We will cover the full install in its own guide.
  • More OCR languages. If you scan mixed-language documents, install the language packs (apt install -y tesseract-ocr-<code>) and update PAPERLESS_OCR_LANGUAGE in /etc/paperless.conf to a +-separated list like eng+deu+fra, then systemctl restart paperless-task-queue.

When Paperless releases a new version you will upgrade by stopping the services, downloading the new tarball, replacing the source files, running pip install -r requirements.txt again, running manage.py migrate, and restarting the services. We will cover that in a follow-up.