Analytics cookies only (Google Analytics, PostHog). No ad networks, no cross-site tracking. See our privacy policy.
A document management system for scanned archives: PDF, TIFF, JPEG and PNG in folders, with document versioning, custom fields, document types, tags, page management, users, groups and granular permissions, OIDC single sign-on and a REST API; OCR and full-text search are added as separate worker services. Apache-2.0, Python with a TypeScript front end; the web app runs as one container next to PostgreSQL (SQLite by default, for demos only).
Runs on a VPS from $6/mo (Kamatera 2GB, 2 GB RAM) · prices checked Sep 2026
Pick Papermerge DMS when your archive is scanned paper rather than living office files: it treats a document as a PDF (TIFF, JPEG and PNG are accepted too) and gives you folders, drag and drop, a dual-panel view, document versioning, custom fields, document types, tags, page-level edits (reorder, rotate, merge, extract), users, groups, granular permissions, OIDC single sign-on and a REST API. It is Apache-2.0 Python with a TypeScript front end, built as microservices you add one at a time: the web app alone first, then PostgreSQL, then optional workers for OCR, Solr search, path templates and S3. Our snippet is the docs' "Persistent Media Storage" compose (web app plus PostgreSQL plus a media volume) with the secret key, admin password and database password generated into `.env`. Upstream's requirements page calls 1 GB the absolute minimum for the web app alone, which is our floor; budget more with PostgreSQL beside it. The catalog's Paperless-ngx ships OCR and consumption folders in its default stack and Papra is the lighter, newer entry; Papermerge sits between them, strongest on versioning and permissions.
Paid link — we earn a commission if you shop through it.
Run these commands on your server:
# Papermerge DMS — document management for scanned archives with versioning and custom fields — based on the "Persistent Media Storage" compose on the docs' Docker Compose page, pinned to 3.5.3
echo "SECRET_KEY=$(openssl rand -hex 32)" > .env
echo "ADMIN_PASSWORD=$(openssl rand -hex 12)" >> .env
echo "DB_PASSWORD=$(openssl rand -hex 16)" >> .env
cat > docker-compose.yml <<'EOF'
services:
webapp:
image: papermerge/papermerge:3.5.3
restart: unless-stopped
environment:
PAPERMERGE__SECURITY__SECRET_KEY: ${SECRET_KEY}
PAPERMERGE__AUTH__USERNAME: admin
PAPERMERGE__AUTH__PASSWORD: ${ADMIN_PASSWORD}
PAPERMERGE__DATABASE__URL: postgresql://pmg:${DB_PASSWORD}@db:5432/pmgdb
PAPERMERGE__MAIN__MEDIA_ROOT: /var/media/pmg
volumes:
- media_root:/var/media/pmg
ports:
- 12000:80
depends_on:
db:
condition: service_healthy
db:
image: postgres:16.1
restart: unless-stopped
environment:
POSTGRES_USER: pmg
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_DB: pmgdb
volumes:
- pgdata:/var/lib/postgresql/data/
healthcheck:
test: pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB
interval: 5s
timeout: 10s
retries: 5
start_period: 10s
volumes:
pgdata:
media_root:
EOF
docker compose up -d
timeout 180 bash -c 'until curl -fsS http://localhost:12000/ >/dev/null; do sleep 5; done'
# → http://SERVER_IP:12000 — sign in as admin with ADMIN_PASSWORD from .envThe docs' Docker Compose and Overview pages flag these before you load real documents: