Skip to Content

Production Guide: Deploy Paperless-ngx with Docker Compose + Caddy + PostgreSQL + Redis on Ubuntu

A production-ready document management stack with OCR, HTTPS, backups, and recovery checks.

Paperless-ngx is a practical win for small teams that are still living with invoice folders, HR PDFs, scanned receipts, and ad-hoc shared drives. Instead of asking everyone to name files perfectly, you can run a private document intake pipeline that watches an inbox, OCRs scans, stores searchable text, and exposes a clean web interface for tagging and retrieval. This guide deploys Paperless-ngx on one Ubuntu server with Docker Compose, Caddy for HTTPS, PostgreSQL for metadata, Redis for queues, plus Tika and Gotenberg for Office document conversion.

Architecture and flow overview

The production pattern is intentionally simple: Caddy terminates TLS and forwards traffic to the Paperless web container; Paperless stores metadata in PostgreSQL, uses Redis for background jobs, writes original documents and thumbnails to a persistent media volume, and calls Tika and Gotenberg when it needs to parse formats such as DOCX, XLSX, and email attachments. Operators place new files in the consume directory, or automate delivery from a scanner, SFTP drop, or mailbox sync job.

Keep the database, media directory, export directory, and consume directory on durable storage. Treat the consume folder as an input queue, not the only copy of important documents. In regulated environments, also plan retention rules, off-site backups, and access reviews before onboarding sensitive departments.

Prerequisites

  • Ubuntu 22.04 or 24.04 server with a static IP and DNS record such as docs.example.com.
  • Docker Engine and Compose plugin installed.
  • Ports 80 and 443 open to the internet for Caddy certificates.
  • At least 2 vCPU, 4 GB RAM, and storage sized for the document archive.
  • A backup destination that is not the same VPS disk.

Step-by-step deployment

1) Create directories and a service user

Use a dedicated directory so backups, permissions, and operational handoffs are predictable. The consume and export paths are separated from application data because they often connect to scanner workflows or one-time migration jobs.

sudo mkdir -p /opt/paperless/{data,media,export,consume,postgres,redis}
sudo chown -R $USER:$USER /opt/paperless
cd /opt/paperless
umask 077
touch .env docker-compose.yml Caddyfile

Manual copy fallback: select the command block above and copy it directly if the button is unavailable.

2) Create the environment file

Generate secrets locally and store them outside version control. Paperless uses the secret key for signing; changing it later can invalidate sessions and break integrations, so keep it in your password manager. The admin account below is for first login only; create named users afterward.

cat > .env <<'EOF'
PAPERLESS_URL=https://docs.example.com
PAPERLESS_SECRET_KEY=replace-with-openssl-rand-hex-32
PAPERLESS_ADMIN_USER=admin
PAPERLESS_ADMIN_PASSWORD=replace-with-long-random-password
[email protected]
POSTGRES_DB=paperless
POSTGRES_USER=paperless
POSTGRES_PASSWORD=replace-with-database-password
PAPERLESS_TIME_ZONE=America/Chicago
PAPERLESS_OCR_LANGUAGE=eng
EOF
openssl rand -hex 32

Manual copy fallback: paste the template, then replace every placeholder before starting the stack.

3) Add Docker Compose services

This Compose file keeps Paperless on an internal network and exposes only Caddy. Tika and Gotenberg are optional for pure PDF workflows, but enabling them early avoids a second migration when users start uploading Office documents. Health checks make restart behavior easier to diagnose.

cat > docker-compose.yml <&lt'EOF'
services:
  broker:
    image: docker.io/library/redis:7-alpine
    restart: unless-stopped
    volumes:
      - ./redis:/data

  db:
    image: docker.io/library/postgres:16-alpine
    restart: unless-stopped
    environment:
      POSTGRES_DB: ${POSTGRES_DB}
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    volumes:
      - ./postgres:/var/lib/postgresql/data

  gotenberg:
    image: docker.io/gotenberg/gotenberg:8
    restart: unless-stopped
    command:
      - gotenberg
      - --chromium-disable-javascript=true

  tika:
    image: docker.io/apache/tika:latest
    restart: unless-stopped

  webserver:
    image: ghcr.io/paperless-ngx/paperless-ngx:latest
    restart: unless-stopped
    depends_on:
      - db
      - broker
      - gotenberg
      - tika
    env_file: .env
    environment:
      PAPERLESS_REDIS: redis://broker:6379
      PAPERLESS_DBHOST: db
      PAPERLESS_TIKA_ENABLED: 1
      PAPERLESS_TIKA_ENDPOINT: http://tika:9998
      PAPERLESS_TIKA_GOTENBERG_ENDPOINT: http://gotenberg:3000
      PAPERLESS_CONSUMER_POLLING: 60
    volumes:
      - ./data:/usr/src/paperless/data
      - ./media:/usr/src/paperless/media
      - ./export:/usr/src/paperless/export
      - ./consume:/usr/src/paperless/consume

  caddy:
    image: docker.io/library/caddy:2-alpine
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - caddy_data:/data
      - caddy_config:/config

volumes:
  caddy_data:
  caddy_config:
EOF

Manual copy fallback: copy the full YAML block and validate indentation with docker compose config.

4) Configure Caddy and start the stack

Caddy should be the only public entry point. Keep Paperless itself unbound from the host network, and let Caddy request certificates automatically after DNS is correct.

cat > Caddyfile <&lt'EOF'
docs.example.com {
  encode zstd gzip
  reverse_proxy webserver:8000
  header {
    Strict-Transport-Security "max-age=31536000; includeSubDomains"
    X-Content-Type-Options "nosniff"
    Referrer-Policy "strict-origin-when-cross-origin"
  }
}
EOF

docker compose config
docker compose pull
docker compose up -d

Manual copy fallback: copy the Caddyfile and start commands separately if your browser strips the clipboard helper.

Configuration and secrets handling

Do not commit .env, exported archives, or the media directory into a Git repository. Store credentials in a password manager, rotate the initial admin password after first login, and create named user accounts with the minimum access required. If multiple teams share one instance, define document types, correspondents, tags, and storage paths before the first bulk import; this prevents a messy taxonomy that becomes hard to clean later.

For scanners, prefer a write-only delivery path. A simple option is an SFTP-only account that lands files in /opt/paperless/consume. For email ingestion, use a dedicated mailbox and an app password, then document who can send to that address. In both cases, test with non-sensitive documents before sending production payroll, legal, or customer records.

Verification

After startup, confirm that containers are healthy, the web UI is reachable, and the consume workflow really creates a searchable document. Use a small test PDF first, then verify OCR text appears in search results.

cd /opt/paperless
docker compose ps
curl -I https://docs.example.com
docker compose logs --tail=100 webserver
printf 'Paperless deployment test\n' > consume/test.txt
sleep 90
docker compose logs --tail=100 webserver | grep -i 'consume\|document\|task'

Manual copy fallback: run these commands one at a time and inspect logs if the consume task is delayed.

A production-ready verification checklist should include login, password reset email if configured, document upload, OCR search, Office document conversion, backup restore, certificate renewal, and a disk-space alert. Do not declare the rollout complete until you have restored at least one database dump and one media file to a test directory.

Backups and recovery

Paperless is only useful if the archive survives operator mistakes and VPS failures. Back up PostgreSQL and the media directory together so metadata and files remain consistent. Keep several daily restore points and at least one off-site copy.

cd /opt/paperless
mkdir -p backups
TS=$(date +%F-%H%M)
docker compose exec -T db pg_dump -U "$POSTGRES_USER" "$POSTGRES_DB" > backups/paperless-$TS.sql
tar -czf backups/paperless-media-$TS.tgz media data export
# Example off-site sync target; replace with your backup host.
rsync -av --delete backups/ [email protected]:/srv/backups/paperless/

Manual copy fallback: copy the backup commands and replace the rsync destination with your approved backup target.

Common issues and fixes

Uploads work but Office files are blank

Check that Tika and Gotenberg containers are running and that the Paperless environment variables point to the internal service names. Restart the webserver after changing conversion settings.

Caddy cannot issue certificates

Confirm the DNS record points to the server, ports 80 and 443 are open, and no other reverse proxy is binding those ports. Use docker compose logs caddy for the ACME error details.

OCR is slow or the queue backs up

Large scans need CPU. Lower scanner DPI for routine documents, add CPU capacity, or schedule bulk imports outside business hours. Monitor Redis and webserver logs during migrations.

Users cannot find documents consistently

Fix the taxonomy, not just the search query. Standardize correspondents, document types, and tag naming. Then process a small migration batch, review results with users, and only then import the full archive.

FAQ

Can I expose Paperless-ngx directly without Caddy?

You can, but it is not recommended. A reverse proxy gives you TLS automation, security headers, and one clean place to add access controls later.

Should I use SQLite instead of PostgreSQL?

SQLite is fine for experiments. PostgreSQL is a better default for multi-user production because backups, monitoring, and concurrency are more predictable.

How much storage should I allocate?

Estimate current document volume, annual growth, and retention requirements, then add room for thumbnails, exports, and backup staging. Monitor disk usage from day one.

Can scanners send directly into the consume folder?

Yes. Use SFTP, SMB, or a small sync job, but avoid giving scanner devices broad shell access. Treat the consume path as an inbox with limited permissions.

What should I back up?

Back up PostgreSQL dumps, the media directory, the data directory, the Compose file, Caddyfile, and the encrypted copy of your environment secrets.

How do I handle sensitive HR or finance documents?

Use named accounts, restrict permissions, document retention rules, and test restore procedures. For highly regulated data, add network allowlists or SSO in front of the service.

Internal links

Talk to us

If you want this implemented with hardened defaults, observability, and tested recovery playbooks, our team can help.

Contact Us

Production Guide: Deploy Joplin Server with Docker Compose + Caddy + PostgreSQL on Ubuntu
A practical, production-oriented Joplin Server deployment with Docker Compose, Caddy TLS, PostgreSQL, backups, verification, and recovery checks.