Cài paperless-ngx trên VPS bằng Docker Compose
Cài paperless-ngx trên VPS với Docker Compose, Postgres và HTTPS. Cấu hình PAPERLESS_URL, thư mục consume, ngôn ngữ OCR và backup đúng cách.
Bạn sẽ xây dựng gì
Paperless-ngx trên VPS biến một thư mục chứa giấy tờ đã scan thành kho lưu trữ có thể tìm kiếm. Bạn thả PDF vào thư mục được theo dõi. Server chạy OCR (nhận dạng ký tự quang học), trích xuất văn bản, đoán ngày và bên liên quan, rồi lưu hồ sơ. Quá trình cài đặt chỉ cần một file Docker Compose với 4 service. Sau đó chủ yếu là cấu hình. Hướng dẫn này dành phần lớn nội dung cho cấu hình vì đây là nơi các bản cài đặt thường lỗi. Paperless-ngx không phải thư viện ảnh: OCR và tính năng đoán bên liên quan không có tác dụng với thư mục chứa các file JPEG chụp trong kỳ nghỉ. Hãy đưa những file đó vào photo server phù hợp và dùng paperless cho giấy tờ. Video cũng tương tự: một thư viện phim đã rip nên nằm trên media server, nơi một giao diện Jellyfin như Jellyfin được thiết kế theo kiểu cửa hàng cho thuê phim thập niên 90 biến việc duyệt phim thành mục đích chính thay vì tìm kiếm.
Paperless-ngx là bản fork do cộng đồng duy trì của dự án Paperless ban đầu. Đây là phần mềm miễn phí, tự host và lưu tài liệu dưới dạng file thông thường trên disk, nên bạn luôn có quyền truy cập vào archive của mình. Chạy nó trên VPS thay vì máy tính ở nhà giúp bạn truy cập các bản scan từ bất kỳ đâu mà không cần mở cổng trên router gia đình. Nó cũng kết hợp tốt với một instance Nextcloud riêng cho các file không ở dạng giấy. Logic tương tự áp dụng cho máy tính đang kết nối với scanner, vì một RustDesk relay tự quản lý trên VPS đó cho phép bạn điều khiển máy này từ nơi khác mà cũng không cần mở cổng trên router.
Stack thực sự chạy những gì
File compose chính thức khởi động 4 container. Biết chức năng của từng container sẽ giúp bạn đọc log dễ hơn.
webserver: image paperless-ngx. Nó chạy web interface, API, consumer theo dõi input folder và các Celery task worker thực hiện OCR.db: PostgreSQL. Nó lưu metadata, tag, correspondent và các bảng index full-text search. Nó không lưu các file PDF của bạn.broker: Valkey, một key-value store tương thích với Redis. Nó làm task queue giữa web process và worker.gotenbergvàtika: tùy chọn, chỉ có trong các biến thể compose-tika. Chúng chuyển đổi tài liệu Office (.docx,.xlsx,.odt) sang PDF để paperless index chúng.
Tính đến tháng 10 năm 2026, file compose cho postgres cố định phiên bản docker.io/library/postgres:18 và docker.io/valkey/valkey:9-alpine, đồng thời pull app từ ghcr.io/paperless-ngx/paperless-ngx:latest.
Voraussetzungen
- Ein Ubuntu-24.04-KVM-VPS mit sudo-Zugriff sowie bereits installiertem Docker und Compose-Plugin. Wenn dieser Teil neu für dich ist, beginne mit den Docker-Compose-Grundlagen für einen VPS und kehre danach hierher zurück.
- Ein Domainname mit einem A-Record, der auf den VPS zeigt. Paperless akzeptiert keine Anmeldung über HTTPS unter einem Hostnamen, der nicht zuvor konfiguriert wurde. Das ist daher früher wichtig, als du vielleicht erwartest.
- Der Arbeitsspeicher ist die eigentliche Einschränkung. PostgreSQL, Valkey, der Granian-Webserver und ein Tesseract-OCR-Worker passen bei geringer Nutzung gleichzeitig in 2 GB. Verwende 4 GB, wenn du einen Rückstand von mehreren hundert Scans importieren möchtest. Die OCR-Verarbeitung einer großen mehrseitigen PDF ist der Speicherspitzenwert, durch den der Kernel einen Worker wegen Speichermangels beendet.
- Speicherplatz: Dein Archiv wird zweimal gespeichert, als Originaldatei und als OCR-Archiv-PDF. Plane daher ungefähr die doppelte Größe deiner Scans ein.
Lấy các file compose chính thức
Có một trình cài đặt tương tác:
bash -c "$(curl --location --silent --show-error https://raw.githubusercontent.com/paperless-ngx/paperless-ngx/main/install-paperless-ngx.sh)"Trình cài đặt này đặt câu hỏi rồi ghi các file cho bạn. Tự thực hiện chỉ cần 4 lệnh và giúp bạn biết rõ mọi thứ nằm ở đâu. Đây là cách phù hợp trên server mà bạn sẽ tiếp tục quản trị.
mkdir -p ~/paperless && cd ~/paperless
curl -fsSL -o docker-compose.yml https://raw.githubusercontent.com/paperless-ngx/paperless-ngx/main/docker/compose/docker-compose.postgres.yml
curl -fsSL -o docker-compose.env https://raw.githubusercontent.com/paperless-ngx/paperless-ngx/main/docker/compose/docker-compose.env
curl -fsSL -o .env https://raw.githubusercontent.com/paperless-ngx/paperless-ngx/main/docker/compose/.envCác biến thể nằm trong cùng một thư mục: docker-compose.sqlite.yml, docker-compose.mariadb.yml và một phiên bản -tika tương ứng cho mỗi biến thể. Chọn postgres khi cài đặt mới. SQLite phù hợp với vài trăm tài liệu, nhưng index tìm kiếm toàn văn sẽ chậm từ lâu trước khi PostgreSQL gặp vấn đề.
File .env chứa một dòng, COMPOSE_PROJECT_NAME=paperless. Tên này trở thành prefix cho mọi container và volume. Vì vậy, đừng xóa file này rồi thắc mắc tại sao docker compose down -v không tìm thấy dữ liệu của bạn.
Cấu hình docker-compose.env trước lần khởi động đầu tiên
Hai thiết lập là bắt buộc. Tạo secret key bằng command được project hướng dẫn:
python3 -c "import secrets; print(secrets.token_urlsafe(64))"Sau đó chỉnh sửa docker-compose.env:
PAPERLESS_SECRET_KEY=<the long string you just generated>
PAPERLESS_URL=https://paperless.example.com
PAPERLESS_TIME_ZONE=Europe/Berlin
PAPERLESS_OCR_LANGUAGE=deu+eng
USERMAP_UID=1000
USERMAP_GID=1000PAPERLESS_SECRET_KEY được phát hành với giá trị nguyên văn change-me. Giá trị này dùng để ký session token. Các release hiện tại xem placeholder đó là chưa được thiết lập: paperless từ chối khởi động khi key bị thiếu, để trống hoặc vẫn là change-me. Vì vậy stack sẽ không khởi động cho đến khi bạn thay giá trị này. Hãy đặt giá trị trước lần khởi động đầu tiên, vì thay đổi sau đó sẽ đăng xuất tất cả user.
PAPERLESS_URL là thiết lập giúp bạn tiết kiệm cả giờ xử lý lỗi. Paperless là một ứng dụng Django. Django kiểm tra cả header Host và origin mà browser báo cáo trên form đăng nhập. Đặt PAPERLESS_URL để Django tự thêm địa chỉ của bạn vào ALLOWED_HOSTS, CORS_ALLOWED_HOSTS và CSRF_TRUSTED_ORIGINS. Nếu để trống rồi trỏ domain vào máy chủ, trang đăng nhập vẫn tải được vì PAPERLESS_ALLOWED_HOSTS mặc định là *. Tuy nhiên, gửi form qua HTTPS sẽ thất bại với CSRF verification failed. Request aborted. vì domain của bạn không phải là trusted origin. Nhập giá trị không có dấu slash ở cuối và không có path.
USERMAP_UID và USERMAP_GID xác định user mà container sử dụng để chạy. Đặt chúng khớp với account của bạn, kiểm tra bằng id -u và id -g. Nếu không khớp, các file bạn copy vào consume folder sẽ không thể đọc được bởi consumer, và log sẽ hiển thị lỗi permission thay vì thực hiện import.
Khởi động stack và tạo user đầu tiên
docker compose pull
docker compose up -d
docker compose run --rm webserver createsuperuser
docker compose logs -f webservercreatesuperuser yêu cầu nhập username, email và password. Không có thông tin đăng nhập mặc định. Nếu bỏ qua bước này, web interface sẽ yêu cầu người đầu tiên mở nó tạo superuser. Khi port 8000 vẫn mở ra Internet, người đó có thể không phải bạn. Vì vậy, hãy tạo account ngay từ shell. Chờ log báo server đang listening trên port 8000 rồi mới thử mở bằng browser. Lần khởi động đầu tiên cũng chạy database migration, thường mất một hoặc hai phút.
Kiểm tra locally trước khi cấu hình domain:
curl -I http://127.0.0.1:8000302 redirect đến /accounts/login/ nghĩa là stack đang hoạt động bình thường.
Đặt HTTPS phía trước dịch vụ
File compose mặc định publish 8000:8000, bind vào mọi interface. Trên một VPS public, cấu hình này cung cấp toàn bộ kho tài liệu của bạn qua HTTP không mã hóa cho bất kỳ ai tìm được địa chỉ. Đổi dòng port để chỉ bind vào loopback:
ports:
- "127.0.0.1:8000:8000"Sau đó thực hiện TLS termination (transport layer security) tại một reverse proxy và forward request đến 127.0.0.1:8000. Nếu đây là app duy nhất trên máy, proxy nào có ACME (automatic certificate management environment) client cũng dùng được. Nếu bạn chạy nhiều container phía sau cùng một cấu hình certificate, hãy làm theo mô hình Traefik reverse proxy cho nhiều app Docker Compose và gắn service webserver vào proxy network mà không publish port nào.
Dù dùng proxy nào, origin check của form đăng nhập là điểm thường làm HTTPS setup lỗi. Paperless bỏ qua X-Forwarded-Proto trừ khi bạn đặt PAPERLESS_PROXY_SSL_HEADER, tùy chọn này mặc định bị tắt. Vì vậy Django coi mọi request đi qua proxy là HTTP thường, còn origin https:// mà browser gửi chỉ được chấp nhận nếu có trong danh sách trusted. Đặt PAPERLESS_URL thành đúng địa chỉ https:// mà bạn nhập trong browser sẽ thêm địa chỉ đó vào danh sách. Nếu thiếu cấu hình này, bạn sẽ gặp CSRF verification failed. Request aborted. trên một trang nhìn vẫn bình thường.
Bạn cũng cần tăng giới hạn upload của proxy. Một file scan 40 MB đi qua proxy chỉ cho phép body tối đa 1 MB sẽ bị từ chối trước khi paperless nhận được. Browser sẽ chỉ báo lỗi upload chung chung.
Cách thư mục consume hoạt động
File compose bind-mount ./consume từ thư mục compose vào container. Mọi tệp bạn đặt vào đó sẽ được import rồi xóa khỏi thư mục, vì tệp lúc này đã nằm trong media volume do paperless quản lý.
cp ~/scan-2026-07-14.pdf ~/paperless/consume/
docker compose logs -f webserverBạn sẽ thấy consumer nhận diện tên tệp, chạy OCR và kết thúc bằng một dòng thông báo tài liệu đã được thêm. Toàn bộ chu trình mất vài giây với bản scan 1 trang và có thể mất 1 phút hoặc lâu hơn với tài liệu dài.
Hai thiết lập thay đổi cách tìm tệp. PAPERLESS_CONSUMER_RECURSIVE=true khiến paperless tìm trong các thư mục con, còn PAPERLESS_CONSUMER_SUBDIRS_AS_TAGS=true biến tên mỗi thư mục con thành một tag. Vì vậy, thả tệp vào consume/invoices/2026/ sẽ gắn cho tệp các tag invoices và 2026. Đây là hệ thống phân loại rẻ nhất mà bạn có thể xây dựng.
Phát hiện tệp là phần còn lại. Mặc định, PAPERLESS_CONSUMER_POLLING_INTERVAL có giá trị 0, nghĩa là paperless sử dụng thông báo filesystem gốc và nhận được chúng ngay lập tức. Các thông báo này không đáng tin cậy trên network filesystem. Nếu thư mục consume của bạn là share NFS hoặc SMB để network scanner có thể ghi tệp vào đó, tệp có thể nằm đó mà không được phát hiện. Cách xử lý là đặt interval thành một số giây dương để paperless quét thư mục thay vì dựa vào thông báo.
Ngôn ngữ OCR và chi phí
PAPERLESS_OCR_LANGUAGE nhận mã Tesseract gồm 3 chữ cái, mặc định là eng. Kết hợp nhiều ngôn ngữ bằng dấu cộng, như trong deu+eng. Sau đó, Tesseract xử lý mọi ngôn ngữ được liệt kê trên từng trang, vì vậy mỗi ngôn ngữ bổ sung đều làm tăng thời gian CPU cho mỗi lần scan. Trên VPS dùng chung vCPU, điều này có thể khiến một lần scan mất 10 giây thay vì 1 phút. Chỉ liệt kê những ngôn ngữ thực sự xuất hiện trong tài liệu của bạn.
Image này có sẵn tiếng Anh, tiếng Đức, tiếng Ý, tiếng Tây Ban Nha và tiếng Pháp. Với các ngôn ngữ khác, thêm ngôn ngữ đó vào PAPERLESS_OCR_LANGUAGES dưới dạng danh sách cách nhau bằng dấu cách, ví dụ PAPERLESS_OCR_LANGUAGES=tur ces, rồi restart. Container tải các gói dữ liệu Tesseract khi khởi động, nên lần boot đầu tiên sau thay đổi này sẽ lâu hơn.
Sao lưu database và media
Sao chép Docker volume khi PostgreSQL đang chạy có thể tạo ra bản sao lưu không thể restore. Paperless có sẵn exporter riêng. Exporter này ghi tài liệu cùng một JSON manifest chứa toàn bộ metadata vào bind mount ./export:
docker compose exec -T webserver document_exporter ../export --delete --no-progress-bar--delete xóa các file đã export nhưng không còn khớp với tài liệu hiện tại, để thư mục luôn là bản mirror thay vì phình to mãi. --no-progress-bar giữ output gọn khi lệnh chạy từ cron, còn -T ngăn Compose yêu cầu terminal mà cron không có.
Để restore, hãy dùng document_importer với chính thư mục đó trên một stack mới. Như vậy, thư mục export là thứ duy nhất bạn phải bảo vệ. Định kỳ gửi thư mục này đến một vị trí ngoài máy bằng restic backup được mã hóa và deduplicate từ VPS của bạn. Hãy chạy export trước để restic không bao giờ capture một archive đang được ghi dở.
Kiểm tra bản sao lưu bằng cách xác nhận export/manifest.json tồn tại và số lượng file khớp với số lượng tài liệu hiển thị trong giao diện. Bản sao lưu mà bạn chưa bao giờ liệt kê thì chưa phải là bản sao lưu. Tệ hơn nữa, export chạy hằng đêm có thể âm thầm fail. Vì vậy, hãy để cron job gửi exit status đến ntfy server của bạn. Bạn sẽ biết trong tuần nó bị hỏng, thay vì chỉ phát hiện vào đúng ngày cần restore.
FAQ
Vì sao mọi trang đều trả về “Bad Request (400)” sau khi tôi trỏ domain vào đó?
Với các file mặc định thì lỗi này không xảy ra, vì PAPERLESS_ALLOWED_HOSTS mặc định là *. 400 kèm DisallowedHost trong log của container cho biết PAPERLESS_ALLOWED_HOSTS đã được đặt thành một danh sách không có domain của bạn. Đặt PAPERLESS_URL=https://paperless.example.com trong docker-compose.env, không thêm dấu gạch chéo ở cuối, để paperless thêm hostname đó vào danh sách, hoặc xóa dòng PAPERLESS_ALLOWED_HOSTS. Sau đó chạy docker compose up -d để tạo lại container. Chỉ sửa file env không có tác dụng, vì container đang chạy vẫn giữ các biến môi trường tại thời điểm nó được khởi động.
Tôi đã thả một file PDF vào thư mục consume nhưng không có gì xảy ra. Lỗi ở đâu?
Trước tiên hãy kiểm tra docker compose logs webserver. Lỗi permission có nghĩa là USERMAP_UID và USERMAP_GID không khớp với account sở hữu file, vì vậy hãy sửa chúng rồi tạo lại container. Nếu hoàn toàn không có dòng log nào, event của file chưa bao giờ được gửi đến. Điều này thường xảy ra trên network share vì filesystem notification native không đáng tin cậy ở đó. Đặt PAPERLESS_CONSUMER_POLLING_INTERVAL thành giá trị như 30 để paperless quét thư mục mỗi 30 giây thay vì chờ notification.
Tôi có thể chạy paperless-ngx với SQLite thay cho PostgreSQL không?
Có. docker-compose.sqlite.yml được hỗ trợ và dùng ít memory hơn, phù hợp với VPS nhỏ. Hạn chế sẽ thấy rõ khi archive tăng lên: full-text search và chỉnh sửa tag hàng loạt chậm đáng kể khi có hàng nghìn tài liệu. Về sau, việc migrate cần export và import. Vì vậy, hãy chọn PostgreSQL ngay nếu bạn dự kiến archive sẽ tiếp tục tăng.
Archive chứa các bản scan thực sự cần bao nhiêu disk?
Khoảng gấp đôi dung lượng của các file nguồn. Paperless giữ nguyên bản gốc và lưu thêm một PDF đã OCR có text layer để tìm kiếm, cùng với các thumbnail nhỏ. Một bản scan chỉ có text, dung lượng 200 KB, vẫn chiếm ít dung lượng. Một bản scan màu của hợp đồng dài, dung lượng 30 MB, sẽ lưu khoảng 60 MB. Nếu giữ thư mục export trên cùng disk, hãy cộng thêm thư mục đó; khi ấy cùng archive sẽ chiếm khoảng gấp 3 dung lượng trên disk.
Tôi có cần các container Tika và Gotenberg không?
Chỉ cần nếu bạn muốn index các file Word, Excel hoặc OpenDocument cùng với PDF. Chúng chuyển đổi các định dạng đó sang PDF để paperless có thể OCR và tìm kiếm nội dung. Chúng cũng thêm 2 container đang chạy và tiêu tốn vài trăm megabyte memory. Vì vậy, hãy bỏ qua chúng trên máy nhỏ nếu mọi thứ bạn lưu trữ đã là PDF hoặc image.