Self-hosting
Your papers, on your own server.
nexpaper runs as a single container with one folder for all its data. You need a machine with Docker, a small home server or a NAS for example, and a short compose file. It needs no Redis, Celery, Postgres or web server of its own, and no account with anybody else.
Starting in three steps
The image lives in the GitHub Container Registry, for amd64 and arm64. It holds the interface, the server and the text recognition in six languages together. nexpaper needs no second database and no web server of its own.
- Create the compose file
Make a folder,
nexpaperfor example, and put this file in it.docker-compose.ymlservices: nexpaper: image: ghcr.io/derkezorm/nexpaper:latest container_name: nexpaper restart: unless-stopped ports: # Only this machine reaches nexpaper (127.0.0.1). Put a reverse proxy # with https in front and let it forward to http://127.0.0.1:8560. # Directly in the home network: write "8560:8000", but only in a # network you trust. Without https, passwords cross it in the clear. - "127.0.0.1:8560:8000" # No rights it does not need: the start only sets the owners of /data # and then drops from root to the user nexpaper. security_opt: - no-new-privileges:true cap_drop: - ALL cap_add: - CHOWN - DAC_READ_SEARCH - SETUID - SETGID volumes: # Database, log, backups and your own languages. A local disk, # never an SMB or NFS share. - ./data:/data # The intake folder for a scanner (Settings, Inputs): # - /path/to/scans:/data/eingang environment: PUID: 1000 PGID: 1000 TZ: Europe/Berlin # Behind a proxy: the address people use and the proxy's own. # NEXPAPER_PUBLIC_URL: https://paper.example.com # NEXPAPER_TRUSTED_PROXIES: 172.16.0.0/12 # Choose the setup code yourself instead of looking for it in the log. # NEXPAPER_SETUP_TOKEN: choose-a-long-one - Start itShell
docker compose up -d - Create the first account
On that machine, open
http://127.0.0.1:8560. The page “Set up nexpaper” asks for a name, a password and the “Setup code”. nexpaper writes the code to the log at every start until it is set up:Shelldocker logs nexpaperThat way nobody takes a fresh instance just because they found it first. Whoever creates the first account becomes the operator. The password needs at least twelve characters, and right after it the account sets up its second factor. Then nexpaper shows eight recovery codes, which you keep safe.
Why the port is bound to 127.0.0.1
With 127.0.0.1:8560:8000 only the machine itself reaches nexpaper. Papers should not sit on a network without https, because without encryption passwords and codes cross the wire in the clear. So a reverse proxy with https belongs in front, forwarding to http://127.0.0.1:8560. If the proxy runs in another container, put both in the same Docker network and forward to http://nexpaper:8000.
A server without a screen
If the server has no browser, write "8560:8000" for the home network and open http://address-of-the-server:8560 from another device. That is meant for a network you trust. The camera, push and passkeys do not work without https anyway, except on the machine itself.
Good to knowWould you rather build from source? Clone the repository. Its docker-compose.yml has build: . instead of image:, explains every option and starts with docker compose up -d --build.
What the machine needs
nexpaper is built for a household or a small firm, not for a data centre. The project names no fixed minimum; here is what is measured or fixed in the code.
- Docker with Compose on Linux, amd64 or arm64. That suits a NAS with Intel, AMD or ARM and a machine with 64-bit ARM.
- A local disk for the data folder. On an SMB or NFS share SQLite does not lock reliably, and the database can lose data. nexpaper warns about it in the settings and in the log at the start.
- Memory: idle, nexpaper took about 100 MB in a measurement on a test server; that is only a rough figure. The image is about 730 MB. Text recognition costs more. It reads one document at a time, and a picture takes about 150 MB while it is unpacked (
NEXPAPER_DECODE_SLOTS). How much it needs at its peak depends on the pages and has not been measured afresh. The operator can limit the memory per document; out of the box it is 2,048 MB. - Space: the documents lie as perfectly ordinary files. The database grows with them: one measurement gave about 21 KB per document for text and search index together, so a household with 1,000 documents a year has about 210 MB after ten years. There is no limit on space per person.
- https through a reverse proxy as soon as anybody comes from outside. The camera, push and passkeys need it in the home network too.
- Phones: nexpaper is a web app and can be put on the home screen from the browser. There is no app from a store. On the iPhone push arrives only in the app on the home screen, from iOS 16.4.
What lies in the data folder
Everything lies in /data, which stands as ./data beside the compose file in the example above.
| Where | What |
|---|---|
nexpaper.db | the SQLite database: accounts, settings, the filing, the recognised text and the search index |
secret.key | the server's secret, which seals the mail and AI credentials and the like |
dokumente/ | the documents themselves as readable files |
backups/, logs/, locales/ | backups, the log and extra languages as JSON files |
cache/, uploads/, exports/ | pictures of pages (trimmed to a size you set), uploads in progress, ZIP files for the tax adviser |
eingang/ | the intake folder when it is switched on: one subfolder per person |
The documents as perfectly ordinary files
Under dokumente/ every document lies as a file in a folder by vault, year and sender, named by date and kind, such as Home/2026/Wiesental Energy/2026-03-14 Invoice.pdf. The original stays exactly as it came in, with a checksum. Beside it lies the searchable copy with the suffix (searchable) and every older version with (version 1). When somebody changes the sender, kind, date or vault, the file follows in the background.
The names of the folders for what is missing are English, whatever language the interface speaks: undated without a date, no sender without a sender, Document without a kind. If you ever stop using nexpaper, you keep your folders.
Documents on the NAS, the database not
The database belongs on a local disk. The documents themselves may lie on a network share: for that you mount /data/dokumente as a volume of its own from the share. nexpaper (PUID) must be allowed to read, write and delete there. In the settings the card “Storage” says whether the database lies locally and whether the documents lie on a share (“That is fine”).
Behind a proxy, with https
nexpaper is best kept in your own network, and on the road you go in through a VPN. If you open it from the internet anyway, go through this list.
- Set it up first, then open it
Create the first account from your own network, with the code from the log. Only then do you forward a port.
- https at the proxy, nexpaper only through it
Leave the port on
127.0.0.1or keep the proxy and nexpaper on a Docker network without a published port. The proxy sends HSTS, not nexpaper. - Name the address and the proxy
NEXPAPER_PUBLIC_URLis the address you reach nexpaper under. Invitation links, the return from the sign-in provider, passkeys and the contact for push come from it; the setting “Public address” in the interface wins if it is set.NEXPAPER_TRUSTED_PROXIESnames the address or network of the proxy. Without it every sign-in seems to come from the proxy, and the protection against guessing takes everybody for one person. - Cookies only over https
NEXPAPER_COOKIE_SECUREisautoout of the box and recognises https also by the proxy'sX-Forwarded-Protoheader.onsuits you if nexpaper is reached over https only; nobody can then sign in over http. - The second factor stays required
Out of the box every account with a password sets one up right after signing in, a code from an authenticator app; a passkey can be added afterwards. The operator could switch that off. On a server on the internet, leave it on.
- Leave shut what you do not need
API keys and the AI are off out of the box. Switch on only what you really use.
- Keep the operator's settings at home
With
NEXPAPER_OPERATOR_NETWORKS: "192.168.0.0/16"nexpaper accepts changes to the operator's settings only from that network, behind a proxy together withNEXPAPER_TRUSTED_PROXIES. - Pin a version
Take a fixed version such as
ghcr.io/derkezorm/nexpaper:1.1.0instead oflatest, update on purpose and back up first. There is also the tag1.1, which moves along with bug fixes.
nexpaper checks itself
Under “Settings”, “Sign-in” the card “Ready for the internet?” stands at the top. Each time you open the page, nexpaper checks eight points itself and says for each one what to do if it is not met: the address with https, the second factor for everybody and for you, the protection against guessing, sessions and cookies, the proxy, the server secrets and the backup. More on the Security page.
On Unraid and TrueNAS
There is a template for Unraid. It needs the same details as the compose file: the port (8000 in the container, 8560 in the example), a folder for /data and the owners of the files. PUID and PGID are usually 99 and 100 on Unraid. You find the setup code in the container's log.
If the template publishes the port to the whole home network, as is usual on Unraid, the same holds as for “8560:8000” above: only in a network you trust, and with a proxy in front for the way out.
For TrueNAS an entry in the catalogue has been submitted. Whether and when it appears there is up to the catalogue's maintainers; until then nexpaper runs there as it does anywhere, as a container from the image.
Good to knowEnter your time zone, such as Europe/Berlin. TZ sets the time in the container's log. People's own time zones come from their browsers.
What you have to back up
Two things that belong together: the backup from nexpaper and the folder with the documents. One alone is not enough.
The backup nexpaper makes itself
Out of the box nexpaper makes a backup every night between three and six o'clock (by the container's clock) and keeps the last seven. Under “Settings”, “Backup” you choose “Weekly” or “Off” and make one by hand with “Back up now”. A backup is a ZIP file with the database, the settings and secret.key, copied cleanly while nexpaper runs. It does not hold the search index, which is built anew after a restore.
The backups lie in /data/backups, so on the same disk. Download one now and then and put it somewhere else.
The documents, which are not in it
The documents themselves are not in the backup. They lie as plain files in /data/dokumente and belong in the backup your server makes anyway, such as Hyper Backup, restic or snapshots. Whoever backs up only the ZIP file from nexpaper has, after a disk failure, the filing without the documents.
Once a week nexpaper compares all files with their checksums and warns if one is missing or has changed. “Check files” stands on the same page.
Checking, restoring, moving
“Check” makes a trial run on a copy and says whether the backup is whole, how many documents it knows, which files are missing on the disk and what a restore would change. “Restore” first backs up the current state and then restarts nexpaper. A backup from a newer version is refused.
To move, you download a backup, upload it on the new server, check and restore it, and copy the documents folder along.
Good to knowThe ZIP file is not encrypted. With secret.key inside, the mail and AI credentials and the addresses of open links can be read. So keep downloaded backups like a password. The steps are in the guide chapter Running it.
Updating
docker compose pull
docker compose up -dAt the start nexpaper adds, step by step, what the database lacks. Before its structure changes it makes a backup by itself, listed as “before a change”. The way back leads through “Settings”, “Backup”.
nexpaper never updates itself. Under “About nexpaper” it says when a new version is out. For that it asks GitHub when somebody opens that page and the last answer is older than a day. This is on out of the box; only the operator can switch “Check once a day” off there.
After an update nexpaper shows every account a notice with a button “What's new?”.
All environment variables
This lists only what has to be settled before the first start. The operator sets everything else in the interface.
| Variable | Default | What for |
|---|---|---|
NEXPAPER_DATA_DIR | /data | Database, documents, log, backups, secrets and languages |
NEXPAPER_LOCALES_DIR | <data>/locales | Extra languages, one JSON file each |
NEXPAPER_SECRET_KEY | made on first start | Protects secrets on the server; when set, it wins over secret.key |
NEXPAPER_PUBLIC_URL | from the request | The address people reach nexpaper under, for invitation links, the sign-in provider, passkeys and push; the setting in the interface wins |
NEXPAPER_TRUSTED_PROXIES | none | Addresses or networks of proxies whose X-Forwarded-For is believed, comma separated |
NEXPAPER_SETUP_TOKEN | made at every start until set up | The code the first account needs |
NEXPAPER_OPERATOR_NETWORKS | none | Networks the operator's settings may be changed from |
NEXPAPER_SESSION_DAYS | 30 | After this many days without use a sign-in with “stay signed in” ends |
NEXPAPER_COOKIE_SECURE | auto | on, off or auto (from the request or X-Forwarded-Proto) |
NEXPAPER_COOKIE_SUFFIX | none | Appended to the cookie names when two nexpapers run on one machine |
NEXPAPER_LOG_LEVEL | setting | quiet, normal, detailed or trace; overrides the setting |
NEXPAPER_DECODE_SLOTS | 1 | How many pictures are unpacked at the same moment, about 150 MB each |
NEXPAPER_UPDATE_URL | GitHub | Where nexpaper asks for the newest version |
NEXPAPER_ARGON2_TIME, NEXPAPER_ARGON2_MEMORY_KIB, NEXPAPER_ARGON2_PARALLELISM | 3, 65536, 2 | Cost of storing passwords; only the tests lower it |
NEXPAPER_FRONTEND_DIST | /app/static | Where the built interface lies; the image sets it |
NEXPAPER_DISABLE_BACKGROUND | false | Switches off the work in the background; for tests only |
NEXPAPER_PORT | 8000 | The port inside the container, needed only when the container runs in host network mode |
PUID, PGID | 1000 | Who owns the files in the data folder; 0 is not allowed, nexpaper never runs as root |
TZ | Europe/Berlin in the example | The container's time zone, for the log; people's own time zones come from their browsers |