> ## Documentation Index
> Fetch the complete documentation index at: https://docs.arct.cloud/llms.txt
> Use this file to discover all available pages before exploring further.

# Self-host Paperless-ngx

> Install Paperless-ngx on an Ubuntu VPS with Docker Compose, HTTPS, OCR-aware sizing, exports, and safe updates.

Arct Cloud provides the unmanaged Linux VPS for this deployment. Paperless-ngx is not preinstalled or managed by Arct Cloud, and you are responsible for the application, document privacy, backups, security, and updates.

<Warning>
  Paperless-ngx stores potentially sensitive documents. Keep its application port private, require HTTPS, use strong account credentials, and maintain encrypted off-server backups.
</Warning>

## Choose a Plan

Paperless-ngx does not publish a single minimum VPS size. OCR speed and memory use vary with document length, image resolution, language packs, concurrent workers, and optional services.

| Arct plan     | vCPU |  RAM | NVMe storage | Suggested use                                            |
| ------------- | ---: | ---: | -----------: | -------------------------------------------------------- |
| **cvm.micro** |    2 | 4 GB |        40 GB | Personal archive and light OCR volume                    |
| **cvm.small** |    4 | 8 GB |        75 GB | Larger imports, multiple users, or faster OCR processing |

These are Arct recommendations rather than upstream requirements. Review current resources on the [Arct Cloud pricing page](https://www.arct.cloud/pricing) and leave capacity for originals, archived PDFs, thumbnails, the database, Redis, and exports.

## Before You Begin

Prepare the following:

* A fresh Ubuntu 24.04 server
* A domain or subdomain such as `documents.example.com`
* An `A` record pointing that hostname to the server's public IPv4 address
* Docker Engine with the Docker Compose plugin, installed from the [official Docker repository](https://docs.docker.com/engine/install/ubuntu/)
* A reverse proxy such as Caddy or Nginx

Paperless-ngx supports several installation routes. This guide uses the project's official PostgreSQL Compose template and manual Compose flow so the port, database password, and application secret can be secured before any container starts.

## Install Paperless-ngx

<Steps>
  <Step title="Deploy and Connect">
    [Deploy an Ubuntu server](/compute/virtual-machines/deploy), then [connect over SSH](/compute/virtual-machines/connect-ssh).
  </Step>

  <Step title="Allow the Ubuntu User to Run Docker">
    After installing Docker Engine and the Compose plugin from Docker's official Ubuntu repository, add the default Ubuntu user to the Docker group:

    ```bash theme={null}
    sudo usermod -aG docker ubuntu
    exit
    ```

    Reconnect so the group membership takes effect, then verify Docker and Compose work without `sudo`:

    ```bash theme={null}
    ssh ubuntu@YOUR_SERVER_IP
    docker version
    docker compose version
    ```

    <Warning>The `docker` group grants effective root access through privileged containers and host mounts. Add only trusted administrator accounts.</Warning>
  </Step>

  <Step title="Download the Official Compose Templates">
    Create the application directory and download the three files from Paperless-ngx's official PostgreSQL Compose example:

    ```bash theme={null}
    sudo install -d -m 0750 -o "$USER" -g "$USER" /opt/paperless
    cd /opt/paperless

    curl -fsSLo docker-compose.yml \
      https://raw.githubusercontent.com/paperless-ngx/paperless-ngx/main/docker/compose/docker-compose.postgres.yml
    curl -fsSLo docker-compose.env \
      https://raw.githubusercontent.com/paperless-ngx/paperless-ngx/main/docker/compose/docker-compose.env
    curl -fsSLo .env \
      https://raw.githubusercontent.com/paperless-ngx/paperless-ngx/main/docker/compose/.env
    ```

    Review these files and compare their URLs with the current [official setup documentation](https://docs.paperless-ngx.com/setup/#docker) before continuing.
  </Step>

  <Step title="Secure the Template Before First Start">
    Patch the official template while it is still offline. This binds Paperless to loopback, replaces the template PostgreSQL password, and passes the same password to the webserver:

    ```bash theme={null}
    cd /opt/paperless
    sed -i 's|      - "8000:8000"|      - "127.0.0.1:8000:8000"|' \
      docker-compose.yml
    sed -i 's|      POSTGRES_PASSWORD: paperless|      POSTGRES_PASSWORD: ${PAPERLESS_DB_PASSWORD:?set PAPERLESS_DB_PASSWORD in .env}|' \
      docker-compose.yml
    sed -i '/      PAPERLESS_DBENGINE: postgresql/a\      PAPERLESS_DBPASS: ${PAPERLESS_DB_PASSWORD:?set PAPERLESS_DB_PASSWORD in .env}' \
      docker-compose.yml
    ```

    Generate separate database and application secrets, configure the public URL, and lock both environment files to the current user:

    ```bash theme={null}
    umask 077
    database_password="$(openssl rand -hex 32)"
    application_secret="$(openssl rand -hex 48)"

    printf '\nPAPERLESS_DB_PASSWORD=%s\n' "$database_password" >>.env
    sed -i "s/^PAPERLESS_SECRET_KEY=.*/PAPERLESS_SECRET_KEY=${application_secret}/" \
      docker-compose.env
    sed -i 's|^#PAPERLESS_URL=.*|PAPERLESS_URL=https://documents.example.com|' \
      docker-compose.env
    unset database_password application_secret

    chmod 0600 .env docker-compose.env
    install -d -m 0700 export
    install -d -m 0750 consume
    ```

    Validate the security-critical changes before pulling or starting anything:

    ```bash theme={null}
    grep -F '127.0.0.1:8000:8000' docker-compose.yml
    grep -F 'PAPERLESS_DB_PASSWORD' docker-compose.yml
    test "$(stat -c '%a' docker-compose.env)" = 600
    test "$(stat -c '%a' .env)" = 600
    test "$(stat -c '%a' export)" = 700

    if grep -Eq '^[[:space:]]*-[[:space:]]*"8000:8000"' docker-compose.yml; then
      echo 'Refusing to start: Paperless would bind to every interface.' >&2
      exit 1
    fi

    docker compose config --quiet
    ```

    <Warning>Do not run `docker compose up` unless every validation above succeeds. The upstream template's unmodified `8000:8000` mapping publishes the first-login screen on every network interface.</Warning>
  </Step>

  <Step title="Start the Private Stack and Create the Superuser">
    Only after the loopback and secret checks pass, start the stack and create the initial superuser from the server terminal:

    ```bash theme={null}
    cd /opt/paperless
    docker compose pull
    docker compose up -d
    docker compose ps
    docker compose run --rm webserver createsuperuser
    ```

    From a second terminal on your local computer, tunnel to the loopback-only service:

    ```bash theme={null}
    ssh -N -L 8000:127.0.0.1:8000 ubuntu@YOUR_SERVER_IP
    ```

    Open `http://127.0.0.1:8000`, sign in with the superuser, and verify the private installation. Stop the tunnel with <kbd>Ctrl</kbd>+<kbd>C</kbd> after the check.
  </Step>

  <Step title="Enable HTTPS">
    Install Caddy using its [official Debian and Ubuntu instructions](https://caddyserver.com/docs/install#debian-ubuntu-raspbian), then add this block to `/etc/caddy/Caddyfile`:

    ```caddy theme={null}
    documents.example.com {
        reverse_proxy 127.0.0.1:8000
    }
    ```

    ```bash theme={null}
    sudo caddy validate --config /etc/caddy/Caddyfile
    sudo systemctl reload caddy
    ```
  </Step>

  <Step title="Verify and Harden the Account">
    Open `https://documents.example.com` and sign in with the superuser created privately. Create a non-superuser account for routine document work, restrict user permissions, and keep the superuser for administration only.

    ```bash theme={null}
    docker compose ps
    docker compose logs --tail=100 webserver
    ```
  </Step>
</Steps>

## Firewall and OCR Controls

Allow SSH before enabling UFW:

```bash theme={null}
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
sudo ufw status
```

Do not expose Paperless port `8000`, PostgreSQL, or Redis publicly. For limited servers, reduce concurrent workers, avoid unnecessary OCR language packs, keep `PAPERLESS_OCR_MODE` at its default, and consider limiting OCR pages or image-cleaning work according to the [upstream low-resource guidance](https://docs.paperless-ngx.com/setup/#less-powerful-devices).

## Back Up and Restore

Pause new document consumption before exporting. The official exporter includes documents, thumbnails, metadata, settings, and database content in the Compose `export` directory. Keep that directory mode `0700` and require it to be empty so old plaintext exports are not mixed into a new recovery set:

```bash theme={null}
cd /opt/paperless
chmod 0700 export
test -z "$(find export -mindepth 1 -maxdepth 1 -print -quit)" || {
  echo 'Move or remove the previous plaintext export before continuing.' >&2
  exit 1
}
docker compose exec -T webserver document_exporter ../export
```

Encrypt the export and deployment configuration before it leaves the VPS. The example below uses an `age` recipient public key; keep the matching private identity off the Paperless server:

```bash theme={null}
sudo apt update
sudo apt install -y age

(
  set -euo pipefail
  cd /opt/paperless
  stamp="$(date -u +%Y%m%dT%H%M%SZ)"
  plaintext="/tmp/paperless-${stamp}.tar.gz"
  encrypted="${plaintext}.age"
  metadata="/tmp/paperless-${stamp}-image.txt"

  docker inspect "$(docker compose ps -q webserver)" \
    --format '{{.Config.Image}} {{.Image}}' >"$metadata"
  tar -czf "$plaintext" \
    export docker-compose.yml docker-compose.env .env \
    -C /tmp "$(basename "$metadata")"
  age -r 'age1REPLACE_WITH_YOUR_BACKUP_RECIPIENT' \
    -o "$encrypted" "$plaintext"
  sha256sum "$encrypted"
  printf 'Encrypted backup ready: %s\n' "$encrypted"
)
```

Copy only the `.age` file to off-server storage and compare its SHA-256 hash at the destination. After the remote copy is verified, remove the short-lived local plaintext, local encrypted staging file, metadata file, and exporter output:

```bash theme={null}
rm -f -- /tmp/paperless-*.tar.gz /tmp/paperless-*.tar.gz.age \
  /tmp/paperless-*-image.txt
find /opt/paperless/export -mindepth 1 -delete
```

Unlinking files cannot guarantee physical erasure on SSD storage, so use encrypted storage and keep plaintext retention as short as possible. API tokens are not included and must be created again after a restore. Exports are version-specific; the encrypted archive includes the exact webserver image metadata and the protected Compose environment files used for that backup.

Restore with the official `document_importer` into an empty installation running a compatible version. Review the [backup and restore documentation](https://docs.paperless-ngx.com/administration/#making-backups) and test the full procedure before relying on it.

## Update Safely

Read the [release notes](https://github.com/paperless-ngx/paperless-ngx/releases) and any referenced migration guide. Stop consumption, create an off-server export, then update the Compose deployment:

```bash theme={null}
docker compose down
docker compose pull
docker compose up -d
docker compose ps
docker compose logs --tail=150 webserver
```

Container startup applies database migrations. Do not interrupt migrations or attempt a downgrade against a migrated database; restore a compatible pre-update backup instead.

## Troubleshooting

| Symptom                                   | Check                                                                                                                                  |
| ----------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| OCR jobs are slow or the server swaps     | Reduce worker concurrency and optional OCR processing, then monitor CPU, RAM, and free disk.                                           |
| Files remain in the consume directory     | Check consumer logs, directory ownership, `USERMAP_UID`/`USERMAP_GID`, and whether the filesystem supports inotify.                    |
| Login redirects to HTTP or the wrong host | Confirm `PAPERLESS_URL` matches the public HTTPS URL and reload the containers.                                                        |
| Caddy returns `502`                       | Confirm the webserver is healthy and bound to `127.0.0.1:8000`.                                                                        |
| An update fails                           | Stop, inspect webserver/database logs and release notes, and restore the matching backup rather than repeatedly restarting migrations. |

## Official Resources

<CardGroup cols={2}>
  <Card title="Paperless-ngx Setup" icon="book-open" href="https://docs.paperless-ngx.com/setup/">
    Official installation routes, Compose templates, and low-resource guidance.
  </Card>

  <Card title="Paperless-ngx on GitHub" icon="github" href="https://github.com/paperless-ngx/paperless-ngx">
    Source code, releases, issues, and security policy.
  </Card>
</CardGroup>

<Note>Paperless-ngx is developed independently of Arct Cloud. Arct Cloud is an independent infrastructure provider and is not affiliated with, sponsored by, or endorsed by the Paperless-ngx project.</Note>
