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 bốn 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 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 tìm kiếm full-text. 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 có thể index chúng.
Tính đến tháng 7 năm 2026, file compose 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 cần
- Một KVM VPS Ubuntu 24.04 có quyền sudo và Docker đã cài sẵn cùng Compose plugin. Nếu phần này còn mới với bạn, hãy bắt đầu với kiến thức nền tảng 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 điều này quan trọng sớm hơn bạn nghĩ.
- Bộ nhớ mới là giới hạn thực tế. PostgreSQL, Valkey, gunicorn và một worker Tesseract OCR cùng chạy thường vẫn vừa trong 2 GB nếu chỉ dùng nhẹ. Hãy cấp 4 GB nếu bạn định import hàng trăm bản scan đang tồn đọng, vì OCR một PDF nhiều trang có thể làm bộ nhớ tăng vọt và khiến kernel OOM killer kill 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 đặ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ó giá trị mặc định là chuỗi literal 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 toàn bộ user.
PAPERLESS_URL là thiết lập giúp bạn tiết kiệm cả giờ xử lý. Paperless là một ứng dụng Django, và Django xác thực header Host trong 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 page sẽ trả về Bad Request (400) và log trong container sẽ ghi DisallowedHost. Ghi giá trị này không có dấu slash ở cuối và không có path.
USERMAP_UID và USERMAP_GID đặt user mà container chạy dưới quyền user đó. Đặ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 copy 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 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, bạn sẽ đến trang đăng nhập nhưng không thể đăng nhập bằng bất kỳ thông tin nào. Chờ dòng log cho biết server đang listening 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 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/ cho biết 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 vào mọi interface. Trên một VPS public, cách này cung cấp toàn bộ kho tài liệu 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 trong reverse proxy và forward đến 127.0.0.1:8000. Nếu đây là ứng dụng duy nhất trên máy, proxy nào có client ACME (automatic certificate management environment) 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ẫu reverse proxy Traefik cho nhiều ứng dụng Docker Compose và kết nối service webserver vào network của proxy mà không publish port.
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, bước kiểm tra origin trên form đăng nhập sẽ fail và bạn nhận CSRF verification failed. Request aborted. trên một trang hiển thị vẫn đúng. Phần còn lại của cách sửa là đặt PAPERLESS_URL thành chính xác địa chỉ https:// mà bạn nhập trong browser.
Đồng thời tăng giới hạn 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, còn browser 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 file bạn đặt vào đó sẽ được import rồi xóa khỏi thư mục, vì lúc này file đã 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 filename, chạy OCR và kết thúc bằng một dòng cho biết document đã được thêm. Toàn bộ chu kỳ thường mất vài giây với bản scan một trang và có thể mất từ một phút trở lên với document dài.
Hai thiết lập thay đổi cách tìm file. 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, đặt file vào consume/invoices/2026/ sẽ gắn cho file các tag invoices và 2026. Đây là hệ thống filing đơn giản nhất bạn có thể xây dựng.
Phát hiện file 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 từ kernel và nhận được thông báo ngay lập tức. Các thông báo này không đi qua network filesystem. Nếu thư mục consume là một NFS hoặc SMB share để network scanner ghi file vào đó, paperless sẽ không phát hiện được file nào. Cách khắc phục là đặt interval thành một số giây dương để paperless quét thư mục thay vì dùng thông báo filesystem.
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, nên mỗi ngôn ngữ bổ sung sẽ làm tăng nhiều lần thời gian CPU cho từng trang. Trên VPS dùng chung vCPU, điều này có thể khiến một bản scan mất mười giây hoặc một phút mới hoàn tấ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 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 sẽ 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ẽ chậm 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ó 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 webserver document_exporter ../export --delete --no-progress-bar--delete xóa các file đã export không còn tương ứng với tài liệu hiện tại, để thư mục luôn là bản mirror thay vì tăng dung lượng mãi. --no-progress-bar giữ cho output gọn khi lệnh này chạy từ cron.
Trên một stack mới, thao tác restore là document_importer từ chính thư mục đó. Vì vậy, bạn chỉ cần bảo vệ thư mục export. Đị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ờ capture một archive chưa ghi xong.
Xác minh backup bằng cách kiểm tra 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. Backup chưa từng được liệt kê không thể xem là backup. Một tác vụ export chạy hằng đêm nhưng âm thầm fail còn tệ hơn. Vì vậy, hãy cấu hình cron job gửi exit status đến ntfy server của bạn. Bạn sẽ phát hiện lỗi ngay trong tuần nó bắt đầu xảy ra, thay vì đến ngày cần restore mới biết.
FAQ
Vì 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ỉ sửa file env thì không có tác dụng, vì container đang chạy vẫn giữ environment mà nó đã khởi động cù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 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 thì event của file chưa bao giờ đến được ứng dụng. Điều này xảy ra với network share vì kernel notification không đi qua được các share đó. Đặt PAPERLESS_CONSUMER_POLLING_INTERVAL thành giá trị như 30 để paperless 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 đánh đổi 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 document. Việc migrate sau này cần export rồi import, nên hãy chọn PostgreSQL ngay nếu bạn dự kiến archive sẽ tiếp tục tăng.
Archive bản scan thực sự cần bao nhiêu dung lượng đĩa?
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 lớp text có thể search, cùng 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 này 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 các format đó sang PDF để paperless có thể OCR và search. 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 thứ bạn lưu trữ đã là PDF hoặc image.