Running Paperless-ngx Behind Traefik with Internal Network Segmentation (Redis + Postgres)

A homelab-backed Paperless-ngx + Traefik deployment with segmented Redis/Postgres networks, concrete checks, and security hardening lessons.

HomeLab

Every document you scan, file, and forget about is sitting somewhere on your network. Receipts, tax forms, medical records — if your self-hosted document manager has a flat network path to the internet, a single container compromise can expose all of it. Running Paperless-ngx behind Traefik with proper network segmentation is the difference between a useful tool and an unintentional data exfiltration point.

TL;DR

  • Keep webserver on both proxy and paperless, and keep db plus broker on paperless only.
  • Route through Traefik labels to the container port 8000; do not publish Paperless host ports.
  • deploy.sh has strong deploy hygiene, but this snapshot still hardcodes weak Paperless defaults in Compose/env files.
  • Treat this as production-ready only after wiring deploy-time secrets into Compose and pinning image versions.

Paperless-ngx is an open-source document management system that ingests, OCRs, tags, and indexes your paper documents and digital files. You point it at a folder (or email inbox), and it turns unstructured paperwork into a searchable, categorized archive backed by Postgres and Redis. For a full introduction to the tool, see our post on what Paperless-ngx is and why you should self-host your documents.

The evidence behind this post

This walkthrough is based on four files in my homelab repo:

  • docker/paperless-ngx/compose.yaml
  • docker/paperless-ngx/docker-compose.env
  • scripts/deploy.sh
  • docs/deployment-guide.md

Everything below maps directly to those files. Anything opinionated is clearly called out as a recommendation.

Architecture: dual-homed app, isolated stateful services

Current Compose shape (trimmed to relevant lines):

services:
  broker:
    image: redis:8
    networks:
      - paperless

  db:
    image: postgres:18
    networks:
      - paperless
    environment:
      POSTGRES_PASSWORD: paperless

  webserver:
    image: ghcr.io/paperless-ngx/paperless-ngx:latest
    env_file:
      - docker-compose.env
    environment:
      PAPERLESS_REDIS: redis://broker:6379
      PAPERLESS_DBHOST: db
    networks:
      - proxy
      - paperless
    labels:
      - traefik.enable=true
      - traefik.http.routers.paperless.rule=Host(`paperless.subdepthtech.org`)
      - traefik.http.routers.paperless.entrypoints=websecure
      - traefik.http.routers.paperless.tls=true
      - traefik.http.routers.paperless.tls.certresolver=cloudflare
      - traefik.http.services.paperless.loadbalancer.server.port=8000

networks:
  proxy:
    external: true
  paperless:
    internal: true

This gives you a clean boundary:

  • Traefik can reach Paperless (webserver on proxy).
  • Postgres and Redis are not attached to proxy.
  • paperless is internal: true, so Docker does not provide external connectivity for that network.

Walkthrough: deploy and verify the segmented setup

1) Keep non-secret app settings in docker-compose.env

Current repo values:

PAPERLESS_TIME_ZONE=America/New_York
PAPERLESS_OCR_LANGUAGE=eng
PAPERLESS_URL=https://paperless.subdepthtech.org
PAPERLESS_SECRET_KEY=change-me
USERMAP_UID=1000
USERMAP_GID=1000

PAPERLESS_URL, timezone, and OCR settings belong here. PAPERLESS_SECRET_KEY=change-me is fine for local bootstrap, not for a hardened deployment.

2) Let the deploy script handle secret materialization

scripts/deploy.sh does several important things correctly:

  • fetches secrets from Proton Pass (pass://homelab/...)
  • writes temporary secrets into a secure temp dir (/dev/shm on Linux, $TMPDIR on macOS)
  • overwrites and deletes temp files on exit
  • deploys with a remote SSH Docker context

Key deployment command excerpt:

docker --context "$CONTEXT_NAME" compose \
  -f "$compose_file" \
  --env-file "$merged_env" \
  up -d --remove-orphans

This is the right pattern for remote deployments without committing secrets to Git.

Important caveat for this specific service: the script currently fetches PAPERLESS_DBPASS and PAPERLESS_SECRET_KEY, but docker/paperless-ngx/compose.yaml does not consume those variables yet.

3) Dry-run and deploy Paperless only

./scripts/deploy.sh paperless-ngx --dry-run
./scripts/deploy.sh paperless-ngx

The same script also ensures the external proxy network exists on the remote Docker host before deployment.

4) Verify runtime state

docker --context homelab-remote compose -f docker/paperless-ngx/compose.yaml ps
docker --context homelab-remote compose -f docker/paperless-ngx/compose.yaml logs --tail 50

Check network attachment explicitly:

WEB_ID=$(docker --context homelab-remote compose -f docker/paperless-ngx/compose.yaml ps -q webserver)
DB_ID=$(docker --context homelab-remote compose -f docker/paperless-ngx/compose.yaml ps -q db)
BROKER_ID=$(docker --context homelab-remote compose -f docker/paperless-ngx/compose.yaml ps -q broker)

docker --context homelab-remote inspect "$WEB_ID" --format '{{json .NetworkSettings.Networks}}'
docker --context homelab-remote inspect "$DB_ID" --format '{{json .NetworkSettings.Networks}}'
docker --context homelab-remote inspect "$BROKER_ID" --format '{{json .NetworkSettings.Networks}}'

Expected result:

  • webserver attached to both proxy and paperless.
  • db and broker attached only to paperless.

Take a moment to check your own Docker stacks. How many of your stateful backends (databases, caches, message brokers) are sitting on the same network as your reverse proxy? If the answer is more than zero, network segmentation should be your next change.

Security notes

  1. What this segmentation protects The internal network keeps Postgres and Redis off the public-facing proxy path, which removes a common exposure class where stateful backends accidentally end up routable.

  2. The main residual risk webserver is the bridge between both networks. If Paperless is compromised, the attacker has a direct path to Redis/Postgres.

  3. Current config mismatch that matters In this snapshot, deploy.sh fetches PAPERLESS_DBPASS and PAPERLESS_SECRET_KEY, but the Paperless Compose config still includes:

    • POSTGRES_PASSWORD: paperless
    • PAPERLESS_SECRET_KEY=change-me in docker-compose.env

    Result: secret retrieval is implemented, but Paperless secret consumption is not fully wired.

  4. Tag stability ghcr.io/paperless-ngx/paperless-ngx:latest, redis:8, and postgres:18 are mutable tag strategies. Good for convenience, weaker for reproducibility and controlled rollbacks.

  5. Practical mitigations

    • use variable substitution in Compose for DB password and secret key
    • remove secret values from tracked env files
    • pin image tags (or digests) and update intentionally
    • keep remote Docker access constrained to a dedicated deploy identity

Lessons learned

  1. Network segmentation is simple to declare and easy to verify, so it should be a default.
  2. Secret retrieval is not secret usage. Fetching from a vault is useless unless Compose consumes those values.
  3. The deploy script already handles hard parts well: preflight checks, idempotent context setup, and cleanup on exit.
  4. The highest-risk failures here are boring defaults (change-me, static DB passwords, floating tags), not exotic exploits.

What I’d do differently

First change: wire deploy-time secrets into compose.yaml so missing values fail fast.

db:
  environment:
    POSTGRES_DB: paperless
    POSTGRES_USER: paperless
    POSTGRES_PASSWORD: ${PAPERLESS_DBPASS:?set_by_deploy}

webserver:
  environment:
    PAPERLESS_DBHOST: db
    PAPERLESS_DBPASS: ${PAPERLESS_DBPASS:?set_by_deploy}
    PAPERLESS_SECRET_KEY: ${PAPERLESS_SECRET_KEY:?set_by_deploy}
    PAPERLESS_REDIS: redis://broker:6379

Second change: stop shipping PAPERLESS_SECRET_KEY=change-me in tracked env files and keep that value vault-only.

Third change: pin deployable image versions (or digests) and promote updates intentionally, not because latest changed overnight.

Summary

  • Dual-network architecture (internal paperless + external proxy) keeps Postgres and Redis off the public-facing path with minimal configuration effort.
  • Route Paperless through Traefik labels instead of publishing host ports, so the web UI gets TLS and centralized routing for free.
  • Secret retrieval in deploy.sh is already implemented, but the Compose file must actually consume those variables to close the loop.
  • Hardcoded defaults like POSTGRES_PASSWORD: paperless and PAPERLESS_SECRET_KEY=change-me are the highest-risk items in this stack, not exotic container escapes.
  • Pin image tags and update intentionally. Mutable tags like :latest trade convenience for reproducibility.

Try It In Your Stack

If you are running Paperless-ngx or a similar document management tool, check whether your database and cache are segmented from your proxy network. It is a small Compose change with a meaningful security payoff. If you have found other ways to harden Paperless deployments — auth layers, backup strategies, ingestion workflows — share what has worked for you.