Skip to content
NEXPAPER

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.

  1. Create the compose file

    Make a folder, nexpaper for example, and put this file in it.

    docker-compose.yml
    services:
      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
  2. Start it
    Shell
    docker compose up -d
  3. 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:

    Shell
    docker logs nexpaper

    That 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.

Set up nexpaper
The page “Set up nexpaper” with the fields Setup code, Name and Password and the button “Create account”.
Setting up the first account.

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.

WhereWhat
nexpaper.dbthe SQLite database: accounts, settings, the filing, the recognised text and the search index
secret.keythe 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.

  1. 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.

  2. https at the proxy, nexpaper only through it

    Leave the port on 127.0.0.1 or keep the proxy and nexpaper on a Docker network without a published port. The proxy sends HSTS, not nexpaper.

  3. Name the address and the proxy

    NEXPAPER_PUBLIC_URL is 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_PROXIES names 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.

  4. Cookies only over https

    NEXPAPER_COOKIE_SECURE is auto out of the box and recognises https also by the proxy's X-Forwarded-Proto header. on suits you if nexpaper is reached over https only; nobody can then sign in over http.

  5. 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.

  6. 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.

  7. 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 with NEXPAPER_TRUSTED_PROXIES.

  8. Pin a version

    Take a fixed version such as ghcr.io/derkezorm/nexpaper:1.1.0 instead of latest, update on purpose and back up first. There is also the tag 1.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

Shell
docker compose pull
docker compose up -d

At 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.

VariableDefaultWhat for
NEXPAPER_DATA_DIR/dataDatabase, documents, log, backups, secrets and languages
NEXPAPER_LOCALES_DIR<data>/localesExtra languages, one JSON file each
NEXPAPER_SECRET_KEYmade on first startProtects secrets on the server; when set, it wins over secret.key
NEXPAPER_PUBLIC_URLfrom the requestThe address people reach nexpaper under, for invitation links, the sign-in provider, passkeys and push; the setting in the interface wins
NEXPAPER_TRUSTED_PROXIESnoneAddresses or networks of proxies whose X-Forwarded-For is believed, comma separated
NEXPAPER_SETUP_TOKENmade at every start until set upThe code the first account needs
NEXPAPER_OPERATOR_NETWORKSnoneNetworks the operator's settings may be changed from
NEXPAPER_SESSION_DAYS30After this many days without use a sign-in with “stay signed in” ends
NEXPAPER_COOKIE_SECUREautoon, off or auto (from the request or X-Forwarded-Proto)
NEXPAPER_COOKIE_SUFFIXnoneAppended to the cookie names when two nexpapers run on one machine
NEXPAPER_LOG_LEVELsettingquiet, normal, detailed or trace; overrides the setting
NEXPAPER_DECODE_SLOTS1How many pictures are unpacked at the same moment, about 150 MB each
NEXPAPER_UPDATE_URLGitHubWhere nexpaper asks for the newest version
NEXPAPER_ARGON2_TIME, NEXPAPER_ARGON2_MEMORY_KIB, NEXPAPER_ARGON2_PARALLELISM3, 65536, 2Cost of storing passwords; only the tests lower it
NEXPAPER_FRONTEND_DIST/app/staticWhere the built interface lies; the image sets it
NEXPAPER_DISABLE_BACKGROUNDfalseSwitches off the work in the background; for tests only
NEXPAPER_PORT8000The port inside the container, needed only when the container runs in host network mode
PUID, PGID1000Who owns the files in the data folder; 0 is not allowed, nexpaper never runs as root
TZEurope/Berlin in the exampleThe container's time zone, for the log; people's own time zones come from their browsers