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.
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
webserveron bothproxyandpaperless, and keepdbplusbrokeronpaperlessonly. - Route through Traefik labels to the container port
8000; do not publish Paperless host ports. deploy.shhas 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.yamldocker/paperless-ngx/docker-compose.envscripts/deploy.shdocs/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 (
webserveronproxy). - Postgres and Redis are not attached to
proxy. paperlessisinternal: 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/shmon Linux,$TMPDIRon 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:
webserverattached to bothproxyandpaperless.dbandbrokerattached only topaperless.
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
-
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.
-
The main residual risk
webserveris the bridge between both networks. If Paperless is compromised, the attacker has a direct path to Redis/Postgres. -
Current config mismatch that matters In this snapshot,
deploy.shfetchesPAPERLESS_DBPASSandPAPERLESS_SECRET_KEY, but the Paperless Compose config still includes:POSTGRES_PASSWORD: paperlessPAPERLESS_SECRET_KEY=change-meindocker-compose.env
Result: secret retrieval is implemented, but Paperless secret consumption is not fully wired.
-
Tag stability
ghcr.io/paperless-ngx/paperless-ngx:latest,redis:8, andpostgres:18are mutable tag strategies. Good for convenience, weaker for reproducibility and controlled rollbacks. -
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
- Network segmentation is simple to declare and easy to verify, so it should be a default.
- Secret retrieval is not secret usage. Fetching from a vault is useless unless Compose consumes those values.
- The deploy script already handles hard parts well: preflight checks, idempotent context setup, and cleanup on exit.
- 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+ externalproxy) 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.shis already implemented, but the Compose file must actually consume those variables to close the loop. - Hardcoded defaults like
POSTGRES_PASSWORD: paperlessandPAPERLESS_SECRET_KEY=change-meare the highest-risk items in this stack, not exotic container escapes. - Pin image tags and update intentionally. Mutable tags like
:latesttrade 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.