Cài Paperless-ngx trên VPS bằng Docker Compose
Hướng dẫn chạy Paperless-ngx trên VPS với Docker Compose, PostgreSQL và HTTPS, gồm PAPERLESS_URL, thư mục consume, OCR, backup và lỗi cấu hình thường gặp.
Bạn đang xây dựng gì
Paperless-ngx trên một VPS biến một thư mục chứa tài liệu giấy đã quét thành kho lưu trữ có thể tìm kiếm. Bạn đưa một tệp 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, phỏng đoán ngày tháng và bên gửi, rồi lưu tệp vào đúng nơi. Quá trình cài đặt chỉ cần một tệp Docker Compose với bốn 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 gặp lỗi.
Paperless-ngx là fork do cộng đồng duy trì của dự án Paperless ban đầu. Phần mềm này miễn phí, tự lưu trữ và lưu tài liệu dưới dạng tệp thông thường trên ổ đĩa, nên bạn luôn có quyền truy cập vào kho lưu trữ của mình. Chạy phần mềm trên VPS thay vì máy tính tại nhà giúp bạn truy cập các bản quét từ bất kỳ đâu mà không cần mở port trên router gia đình, đồng thời hoạt động tốt với một instance Nextcloud riêng cho các tệp không phải giấy.
Stack thực sự chạy 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 giao diện web, API, consumer theo dõi thư mục input và các Celery task worker thực hiện OCR.db: PostgreSQL. Nó lưu metadata, tag, correspondent và các bảng chỉ mục tìm kiếm toàn văn. Nó không lưu các file PDF của bạn.broker: Valkey, một kho key-value tương thích với Redis. Nó làm task queue giữa tiến trình web và các 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 lập chỉ mục.
Tính đến tháng 7 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.
Điều kiện tiên quyết
- Một VPS KVM Ubuntu 24.04 có quyền sudo và Docker đã cài sẵn cùng plugin Compose. Nếu phần này còn mới với bạn, hãy bắt đầu bằng kiến thức cơ bản về Docker Compose cho VPS rồi quay lại.
- Một tên miền có bản ghi A trỏ đến VPS. Paperless từ chối phục vụ trên hostname chưa được cấu hình, nên vấn đề này quan trọng sớm hơn bạn nghĩ.
- Bộ nhớ là giới hạn chính. PostgreSQL, Valkey, gunicorn và một worker OCR Tesseract chạy đồng thời vẫn vừa trong 2 GB đối với nhu cầu nhẹ. Cấp 4 GB nếu bạn dự định import hàng trăm bản scan đang tồn đọng, vì OCR một PDF nhiều trang lớn có thể làm bộ nhớ tăng đột biến và khiến kernel OOM killer dừng worker.
- Disk: archive của bạn được lưu thành 2 bản: file gốc và PDF archive đã chạy OCR. Vì vậy, hãy dự trù dung lượng gần gấp đôi kích thước các bản scan.
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 hỏi thông tin và tự ghi các file cho bạn. Làm thủ công chỉ cần 4 lệnh và giúp bạn biết rõ mọi thứ nằm ở đâu. Đây là điều cần thiết khi bạn quản trị một server lâu dài.
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ỗi biến thể đều có một phiên bản -tika. Chọn postgres cho lần cài đặt mới. SQLite phù hợp với vài trăm tài liệu, nhưng chỉ mục tìm kiếm toàn văn sẽ chậm trước PostgreSQL khá lâu.
File .env chứa một dòng là COMPOSE_PROJECT_NAME=paperless. Tên này trở thành tiền tố của mọi container và volume. Vì vậy, đừng xóa nó 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 lệnh được dự án tài liệu hóa:
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. Nó dùng để ký session cookie. Nếu giữ nguyên, bất kỳ ai biết giá trị mặc định đều có thể giả mạo session. Hãy đặt giá trị này trước lần khởi động đầu tiên, vì thay đổi sau đó sẽ đăng xuất tất cả người dùng.
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, và Django xác thực header Host của mọi request. Đặt PAPERLESS_URL, hệ thống sẽ tự điền ALLOWED_HOSTS, CORS_ALLOWED_HOSTS và CSRF_TRUSTED_ORIGINS cho bạn. Nếu để trống, trỏ một domain đến máy chủ, mọi trang sẽ trả về Bad Request (400) và log của container sẽ ghi DisallowedHost. Nhập giá trị này không có dấu gạch chéo ở cuối và không có path.
USERMAP_UID và USERMAP_GID đặt user mà container chạy dưới quyền đó. Đặt chúng khớp với account của bạn, được kiểm tra bằng id -u và id -g. Nếu không khớp, các file bạn sao chép vào thư mục consume sẽ không thể được consumer đọc, và log sẽ hiển thị lỗi permission thay vì thực hiện import.
Khởi động stack và tạo người dùng đầ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, bạn sẽ gặp trang đăng nhập nhưng không thể đăng nhập. Chờ log báo server đang lắng nghe trên port 8000 rồi mới mở trình duyệt. Lần khởi động đầu tiên cũng chạy các migration của database. Quá trình này mất khoảng 1–2 phút.
Kiểm tra cục bộ 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 ứng dụng
File compose mặc định publish 8000:8000 và bind trên 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 thấy địa chỉ. Đổi dòng port để chỉ bind trên loopback:
ports:
- "127.0.0.1:8000:8000"Sau đó terminate TLS (bảo mật tầng truyền tải) tại một reverse proxy và forward đến 127.0.0.1:8000. Nếu đây là ứng dụng duy nhất trên máy, bạn có thể dùng bất kỳ proxy nào có client ACME (môi trường quản lý certificate). Nếu chạy nhiều container phía sau cùng một cấu hình certificate, hãy làm theo mẫu reverse proxy Traefik cho nhiều ứng dụng Docker Compose và gắn service webserver vào network của proxy mà không publish port nào.
Dù dùng proxy nào, proxy cũng phải gửi X-Forwarded-Proto: https. Nếu thiếu header này, Django sẽ cho rằng request đến qua HTTP, kiểm tra origin trên form đăng nhập sẽ thất bại, và bạn nhận được CSRF verification failed. Request aborted. trên một trang có giao diện đúng. Nửa còn lại của cách sửa này là đặt PAPERLESS_URL thành đúng địa chỉ https:// mà bạn nhập trong browser.
Bạn cũng phải tăng giới hạn kích thước upload của proxy. Một bản scan 40 MB đi qua proxy giới hạn body ở 1 MB sẽ bị từ chối trước khi paperless nhận được, và browser sẽ báo lỗi upload chung chung.
Cách hoạt động của thư mục consume
Tệp compose bind-mount ./consume từ thư mục compose vào container. Mọi tệp bạn đặt trong đó 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 tên tệp, chạy OCR và kết thúc bằng một dòng cho biết tài liệu đã được thêm. Toàn bộ chu trình mất vài giây với bản scan một trang và có thể mất một 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 tag invoices và 2026. Đây là hệ thống lưu trữ 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. Theo mặc định, PAPERLESS_CONSUMER_POLLING_INTERVAL là 0, nghĩa là paperless sử dụng thông báo filesystem từ kernel và các thông báo này được phát ngay lập tức. Những thông báo đó không đi qua network filesystem. Nếu thư mục consume của bạn là một NFS hoặc SMB share để network scanner có thể ghi vào đó, sẽ không có tệp nào được phát hiện. Cách khắc phục là đặt interval thành 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 ba chữ cái, mặc định là eng. Kết hợp các ngôn ngữ bằng dấu cộng, như trong deu+eng. Sau đó Tesseract thử từng ngôn ngữ và giữ lại kết quả tốt nhất. Vì vậy, mỗi ngôn ngữ bổ sung sẽ làm thời gian CPU cần cho mỗi trang tăng lên. Trên VPS dùng chung vCPU, điều này có thể khiến quá trình quét mất một phút thay vì mười giây. Chỉ liệt kê những ngôn ngữ thực sự có trong tài liệu của bạn.
Image 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 khởi động lại. Container tải các gói dữ liệu Tesseract khi khởi động, nên lần khởi động đầu tiên sau thay đổi này sẽ lâu hơn.
Sao lưu cơ sở dữ liệu và tệp media
Sao chép Docker volume khi PostgreSQL đang chạy sẽ tạo ra bản sao lưu có thể không khôi phục được. Paperless có exporter riêng. Exporter này ghi tài liệu cùng manifest JSON chứa toàn bộ metadata vào bind mount ./export:
docker compose exec webserver document_exporter ../export --delete --no-progress-bar--delete xóa các tệp đã export không còn khớp với tài liệu hiện tại. Nhờ đó, thư mục luôn là bản mirror thay vì tăng kích thước không giới hạn. --no-progress-bar giữ cho output sạch khi lệnh này chạy từ cron.
Khôi phục là document_importer từ chính thư mục đó trên một stack mới. Vì 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í offsite bằng restic backup được mã hóa và deduplicate từ VPS của bạn, đồng thời chạy export trước để restic không bao giờ ghi lại một archive chưa hoàn tất.
Xác minh bản sao lưu bằng cách kiểm tra export/manifest.json tồn tại và số lượng tệp khớp với số tài liệu hiển thị trong giao diện. Bản sao lưu mà bạn chưa từng liệt kê không phải là bản sao lưu.
FAQ
Tại sao mọi trang đều trả về "Bad Request (400)" sau khi tôi trỏ domain vào đó?
Django từ chối header Host vì domain của bạn chưa có trong ALLOWED_HOSTS. Đặt PAPERLESS_URL=https://paperless.example.com trong docker-compose.env, không thêm dấu gạch chéo ở cuối, rồi chạy docker compose up -d để tạo lại container. Chỉ chỉnh sửa file env thì không có tác dụng vì container đang chạy vẫn giữ environment được nạp khi 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, 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ó log line, sự kiện file chưa bao giờ đến được container. Điều này xảy ra trên network share vì kernel notification không đi qua được các share đó. Đặt PAPERLESS_CONSUMER_POLLING_INTERVAL thành giá trị như 30, khi đó paperless sẽ quét thư mục mỗi 30 giây.
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 một VPS nhỏ. Điểm bất lợi xuất hiện khi archive tăng lên: full-text search và chỉnh sửa tag hàng loạt sẽ chậm thấy rõ khi có hàng nghìn document. Sau này chuyển sang hệ quản trị khác 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.
Một archive chứa các bản scan thực tế cần bao nhiêu dung lượng 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 với text layer có thể tìm kiếm, cùng các thumbnail nhỏ. Một bản scan chỉ có text, dung lượng 200 KB, vẫn khá nhỏ. Một bản scan màu của hợp đồng dài, dung lượng 30 MB, sẽ chiếm 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 gấp ba dung lượng 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 các định dạng đó thành 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à vài trăm megabyte memory, vì vậy hãy bỏ qua chúng trên máy nhỏ nếu mọi file bạn lưu trữ đã là PDF hoặc image.