Tự host ERPNext trên VPS bằng Docker
Chạy ERPNext trên VPS của bạn với Docker: tính cấu hình, stack 11 container, TLS, email outbound, pin version và kiểm tra restore backup.
Bạn đang đăng ký chạy gì
Tự host ERPNext trên VPS là công việc vận hành, không phải thao tác cài đặt bằng một lệnh. Stack Docker Compose chính thức gồm 11 container và lưu sổ cái tổng hợp cùng hồ sơ khách hàng của bạn. Vì vậy, mọi yêu cầu bên dưới đều cao hơn: một bản backup chưa được restore thì chưa thể xem là backup, còn image tag không được pin là nguy cơ migration schema có thể xảy ra bất cứ lúc nào.
Một số tên sẽ xuất hiện xuyên suốt tài liệu. ERPNext là ứng dụng nghiệp vụ. Frappe là Python framework bên dưới ứng dụng này. Bench là công cụ dòng lệnh quản lý các site và đã được cài sẵn bên trong các container. Một site là một tenant: gồm một database MariaDB và một thư mục chứa các file đã upload. Hầu hết lệnh trong tài liệu này chạy bench bên trong container backend trên một site được chỉ định bằng tên.
Hướng dẫn này sử dụng repository frappe_docker, là deployment do dự án duy trì. Mọi lệnh bên dưới đã được kiểm tra với repository đó vào tháng 8 năm 2026. Nếu bạn chưa quen với Docker Compose, chạy Docker Compose trên VPS trình bày các kiến thức nền mà hướng dẫn này giả định bạn đã có.
ERPNext cần VPS ở mức nào?
The data behind this chart
[
{
"label": "Evaluation",
"vcpu": 2,
"ram_gb": 4,
"disk_gb": 40
},
{
"label": "Small production",
"vcpu": 4,
"ram_gb": 8,
"disk_gb": 100
},
{
"label": "Room to grow",
"vcpu": 4,
"ram_gb": 16,
"disk_gb": 160
}
]Tài liệu hướng dẫn được công bố bắt đầu từ 2 vCPU và 4 GB RAM trước khi có người dùng đăng nhập. Đây là mức dành cho đánh giá. Các con số này chỉ là điểm bắt đầu, không phải số đo từ hướng dẫn này; khối lượng tài liệu thực tế của bạn mới quyết định con số cần dùng. Hàng cuối hoàn toàn không phải mức tối thiểu được công bố. Đó là mức gần như đủ để bạn không còn phải thường xuyên nghĩ về bộ nhớ.
Hãy đánh giá thực tế các gói nhỏ. VPS 1 GB hoặc 2 GB sẽ khởi động được stack, rồi nhanh chóng lỗi ở lần import đầu tiên hoặc báo cáo chạy lâu đầu tiên. Chín container chạy lâu, buffer pool của MariaDB và một Python worker đang tạo báo cáo không thể cùng nằm trong lượng bộ nhớ đó. Lỗi xảy ra đột ngột. Kernel out of memory killer dừng một container, rồi docker inspect trên container đó hiển thị "OOMKilled": true với exit code 137. Worker bị dừng giữa job khiến một document đã submit chỉ hoàn tất một phần công việc chạy nền.
Với một công ty sử dụng ERPNext hằng ngày, 8 GB RAM, 4 vCPU và 100 GB SSD là mức sàn thực tế. RAM thường cạn trước. Disk tăng nhanh hơn dự kiến vì mọi file đính kèm và mọi bản backup cục bộ đều nằm trên cùng volume với database.
Mười một container và chức năng của từng container
Chạy docker compose ps sau khi stack khởi động và chín container đang chạy. Hai container còn lại, configurator và create-site, thực hiện công việc một lần rồi thoát. Vì vậy tổng số container là mười một.
backendchạy ứng dụng Frappe bằng gunicorn.benchnằm trong container này.frontendlà nginx. Nó phục vụ các static asset và chuyển tiếp mọi yêu cầu khác đến backend.queue-shortvàqueue-longlà các worker RQ (Redis Queue). Chúng chạy các job nền như gửi email, import và tạo report.schedulerchạy các job theo lịch, bao gồm report định kỳ và tài liệu tự động lặp lại.websocketlà tiến trình socket.io xử lý cập nhật trực tiếp trong trình duyệt.dblà MariaDB.redis-cachevàredis-queuelà 2 instance Redis riêng biệt: một instance dùng cho cache và một instance dùng cho job queue.
Bạn nên hiểu cách tách này vì nó cho biết cần đọc log nào. Email bị kẹt là vấn đề của queue worker, vì vậy docker compose logs -f queue-short là command phù hợp. Trang tải được nhưng badge thông báo không bao giờ cập nhật là vấn đề websocket. Đọc log của backend cho một trong hai trường hợp này chỉ làm mất cả buổi chiều.
Cài bằng các file compose production, không dùng bản demo
Repository cung cấp pwd.yml, và README nêu rõ: "Cấu hình này chỉ dành cho việc đánh giá trong thời gian ngắn. Bạn sẽ không thể cài ứng dụng tùy chỉnh vào cấu hình này." Bạn có thể dùng nó để xem xét ERPNext trong một buổi chiều. Không dùng nó để vận hành công ty.
sudo apt update && sudo apt install -y git
curl -fsSL https://get.docker.com | bash
git clone https://github.com/frappe/frappe_docker
cd frappe_docker
mkdir -p ~/gitops
cp example.env ~/gitops/erpnext.envMở ~/gitops/erpnext.env và thay đổi 4 giá trị. ERPNEXT_VERSION cố định image tag. DB_PASSWORD trong file ví dụ có giá trị 123. SITES_RULE là rule định tuyến của Traefik, còn LETSENCRYPT_EMAIL nhận cảnh báo về certificate.
ERPNEXT_VERSION=v16.32.1
DB_PASSWORD=<a long random password>
SITES_RULE=Host(`erp.example.com`)
LETSENCRYPT_EMAIL=ops@example.comBây giờ render một compose file, rồi khởi động file đó.
docker compose --project-name erpnext \
--env-file ~/gitops/erpnext.env \
-f compose.yaml \
-f overrides/compose.mariadb.yaml \
-f overrides/compose.redis.yaml \
-f overrides/compose.https.yaml \
config > ~/gitops/erpnext.yaml
docker compose --project-name erpnext -f ~/gitops/erpnext.yaml up -dconfig không khởi động gì cả. Lệnh này merge file cơ sở với các file override và in kết quả sau khi đã thay thế toàn bộ variable. Sau đó chạy file đã render. Bước bổ sung này rất đáng làm: running stack nằm trong một file duy nhất để bạn đọc và commit, nên nó không thể tự thay đổi khi ai đó sửa env file hoặc khi bạn pull repository. cách merge nhiều Docker Compose file giải thích chi tiết các rule override.
Chờ db khởi động và configurator thoát, quá trình này mất vài giây, rồi tạo site.
docker compose --project-name erpnext exec backend \
bench new-site --mariadb-user-host-login-scope=% \
--db-root-password '<your DB_PASSWORD>' \
--install-app erpnext \
--admin-password '<a strong admin password>' \
erp.example.comKiểm tra:
docker compose --project-name erpnext ps
docker compose --project-name erpnext exec backend bench --site erp.example.com list-appslist-apps phải in frappe và erpnext cùng version của chúng. Một ps hoạt động bình thường sẽ hiển thị 9 service ở trạng thái running và không có service nào ở trạng thái restarting.
Có 2 lỗi thường xảy ra ở đây. --mariadb-user-host-login-scope=% là bắt buộc khi chạy dưới Docker. App container kết nối đến MariaDB qua Docker network, nên nó được xem là một remote host; user database chỉ được cấp quyền cho localhost sẽ không thể đăng nhập từ đó. Khi đó, việc tạo site sẽ fail với lỗi MariaDB access denied và nêu tên user root. Scope % cấp cho user của site mới quyền truy cập từ mọi host trên private network đó.
Lỗi thứ hai là tên site. Theo mặc định, frontend chọn site cần phục vụ dựa trên HTTP Host header, nên site được tạo với tên erpnext sẽ không truy cập được tại erp.example.com dù cả 2 site đều tồn tại. Đặt tên site theo domain như trên, hoặc đặt FRAPPE_SITE_NAME_HEADER trong env file thành tên site rồi render lại compose file.
HTTPS và điều kiện cần có để HTTPS hoạt động
Override compose.https.yaml chạy Traefik trên cổng 443, chuyển hướng cổng 80 sang đó và yêu cầu chứng chỉ từ Let's Encrypt. TLS (bảo mật tầng truyền tải) bảo vệ hóa đơn và session cookie, không để chúng truyền trên mạng dưới dạng plain text.
Hai điều kiện phải đúng, nếu không sẽ không có chứng chỉ nào được cấp. Bản ghi DNS A cho erp.example.com phải trỏ đến VPS. Cổng 80 và 443 phải truy cập được từ Internet, vì Let's Encrypt xác minh bạn kiểm soát tên miền bằng HTTP-01 challenge trên cổng 80. Kiểm tra network firewall của nhà cung cấp và firewall trên máy chủ. Đây là hai lớp kiểm soát riêng biệt, và firewall trong panel là thứ mọi người thường quên.
Chứng chỉ được lưu trong volume cert-data tại /letsencrypt/acme.json. Nếu trình duyệt hiển thị chứng chỉ mặc định thay vì chứng chỉ của bạn, hãy tìm tên proxy service trong docker compose --project-name erpnext ps rồi đọc log của service đó để tìm lỗi ACME (automatic certificate management environment). Bạn đang chạy các web app khác trên cùng server? một Traefik instance đứng trước nhiều app Docker Compose hướng dẫn cách dùng chung proxy thay vì tranh chấp cổng 443.
Email gửi đi, nếu không thì hóa đơn không bao giờ rời khỏi máy chủ
Đây là bước mà phần lớn hướng dẫn ERPNext bỏ qua, nhưng nó quyết định hệ thống có sử dụng được hay không. Nếu email gửi đi không hoạt động, hóa đơn không đến được khách hàng, email đặt lại mật khẩu không đến nơi và báo cáo theo lịch không được gửi. Stack này không có mail server.
Không gửi mail trực tiếp từ VPS qua port 25. Hầu hết nhà cung cấp chặn outbound port 25 trên các tài khoản mới. Những email gửi được cũng thường bị từ chối hoặc đưa vào spam vì địa chỉ VPS mới chưa có uy tín gửi mail. Hãy dùng relay có xác thực qua port 587.
Cách được hỗ trợ là dùng màn hình Email Account trong giao diện ERPNext. Màn hình này lưu password dưới dạng mã hóa. Bạn cũng có thể ghi các key vào cấu hình site:
docker compose --project-name erpnext exec backend \
bench --site erp.example.com set-config mail_server smtp.example.com
docker compose --project-name erpnext exec backend \
bench --site erp.example.com set-config mail_port 587 --parse
docker compose --project-name erpnext exec backend \
bench --site erp.example.com set-config use_tls 1 --parse
docker compose --project-name erpnext exec backend \
bench --site erp.example.com set-config mail_login 'erp@example.com'
docker compose --project-name erpnext exec backend \
bench --site erp.example.com set-config auto_email_id 'erp@example.com'--parse lưu 587 dưới dạng số thay vì chuỗi "587". Đọc lại file và xác nhận 2 giá trị đó không có dấu ngoặc kép:
docker compose --project-name erpnext exec backend \
cat sites/erp.example.com/site_config.jsonĐặt mail_password trong màn hình Email Account thay vì trên command line. Như vậy giá trị được lưu dưới dạng mã hóa và không xuất hiện trong shell history.
Sau đó gửi một email thật. Tạo một Sales Invoice, gửi email đến địa chỉ bạn kiểm soát và theo dõi queue trong lúc thực hiện:
docker compose --project-name erpnext logs -f queue-shortMail gửi đi là background job. Vì vậy, nếu email không bao giờ đến nơi, log thường ghi nhận một failed job thay vì hiển thị lỗi trong trình duyệt. Đồng thời, hãy publish các record SPF (sender policy framework) và DKIM (domainkeys identified mail) cho domain gửi mail, rồi thêm policy DMARC. Nếu thiếu các record này, hóa đơn dù được tạo đúng vẫn có thể vào thư mục spam của khách hàng. Nếu muốn tự quản lý toàn bộ đường đi của email, mail server Mailcow tự host cung cấp một relay do bạn kiểm soát, chạy trên một máy riêng với ERP.
Bản backup có thể restore được
Chỉ dump database thì chưa phải là backup của ERPNext. File đính kèm và file riêng tư nằm trong thư mục sites, không nằm trong MariaDB. Nếu chỉ restore database, mọi purchase order đã upload sẽ trở thành link hỏng.
docker compose --project-name erpnext exec backend \
bench --site erp.example.com backup --with-filesLệnh này ghi 4 file vào sites/erp.example.com/private/backups trong volume sites:
- dump
-database.sql.gz - archive
-files.tarcủa các file public - archive
-private-files.tarcủa các file private - bản sao
-site_config_backup.jsoncủa cấu hình site
File thứ tư thường bị bỏ đi, nhưng lại là file gây hậu quả nghiêm trọng nhất. File này chứa encryption_key, key mà Frappe dùng để mã hóa password đã lưu: thông tin xác thực tài khoản email, key của payment gateway và mọi secret của integration. Restore database mà không có key tương ứng thì site vẫn tải bình thường, nhưng gửi mail sẽ fail với lỗi:
frappe.exceptions.ValidationError: Encryption key is invalid! Please check site_config.jsonLuôn giữ cả 4 file cùng nhau.
Sau đó, đưa chúng ra khỏi server. Backup nằm trong volume sẽ không tồn tại nếu server gặp sự cố, và bench cũng tự xóa chúng: theo mặc định, bench xóa các backup cũ hơn 24 giờ trong thư mục đó.
docker compose --project-name erpnext cp \
backend:/home/frappe/frappe-bench/sites/erp.example.com/private/backups \
~/erpnext-backupsChạy lệnh này từ cron, rồi push thư mục đến một nơi bạn không quản trị. backup restic được mã hóa đến storage off-site là công cụ phù hợp, vì nó mã hóa dữ liệu trước khi upload và restic check xác nhận repository vẫn đọc được. Backup ERP là bản sao của toàn bộ sổ sách, nên phải được mã hóa khi lưu trữ trên phần cứng không phải server này.
Kiểm tra restore trước khi cần đến nó
Một bản backup chưa được kiểm tra chỉ là phỏng đoán. Hãy restore vào site thứ hai trên cùng máy chủ, tuyệt đối không restore vào site đang chạy thật.
docker compose --project-name erpnext exec backend \
bench new-site --mariadb-user-host-login-scope=% \
--db-root-password '<your DB_PASSWORD>' \
--admin-password '<a strong admin password>' \
restore-test.example.com
docker compose --project-name erpnext exec backend \
bench --site restore-test.example.com --force restore \
sites/erp.example.com/private/backups/<stamp>-erp.example.com-database.sql.gz \
--with-public-files sites/erp.example.com/private/backups/<stamp>-erp.example.com-files.tar \
--with-private-files sites/erp.example.com/private/backups/<stamp>-erp.example.com-private-files.tar \
--db-root-password '<your DB_PASSWORD>'Copy encryption key từ config đã backup sang site được restore. Nếu không, các integration của site sẽ tiếp tục bị lỗi:
docker compose --project-name erpnext exec backend \
bench --site restore-test.example.com set-config encryption_key '<value from site_config_backup.json>'Bây giờ hãy kiểm tra restore như một kế toán sẽ làm. Mở báo cáo Accounts Receivable và so sánh số dư cuối kỳ với site đang chạy thật. Mở một purchase invoice gần đây và tải attachment của invoice đó xuống. Một site chỉ hiển thị được trang đăng nhập thì chưa chứng minh được điều gì.
Xóa site kiểm thử sau khi hoàn tất:
docker compose --project-name erpnext exec backend \
bench drop-site restore-test.example.comVì sao cần pin version nghiêm ngặt hơn với ERPNext
Trên một static site, image tag không được pin chỉ gây ra một lần restart ngoài dự kiến. Với ERPNext, điều đó có nghĩa là chạy schema migration. bench migrate ghi đè các bảng database và có thể ghi đè dữ liệu document, đồng thời không có thao tác hoàn tác. Rollback là khôi phục từ backup, không phải một docker compose down.
Vì vậy, hãy pin tag. ERPNEXT_VERSION=v16.32.1 là release được pin trong pwd.yml của chính repository vào tháng 8 năm 2026. Không dùng tiếp số version đó mà không kiểm tra. Các release hiện tại được liệt kê trên trang releases của frappe/erpnext, còn các image tag đang tồn tại có trên Docker Hub. Hãy đọc release note của version bạn sắp chuyển sang trước khi thực hiện.
Quy trình upgrade bắt đầu bằng backup và maintenance mode.
docker compose --project-name erpnext exec backend \
bench --site erp.example.com backup --with-files
docker compose --project-name erpnext exec backend \
bench --site erp.example.com set-maintenance-mode onSửa ERPNEXT_VERSION trong ~/gitops/erpnext.env, sau đó render, pull và migrate.
docker compose --project-name erpnext \
--env-file ~/gitops/erpnext.env \
-f compose.yaml \
-f overrides/compose.mariadb.yaml \
-f overrides/compose.redis.yaml \
-f overrides/compose.https.yaml \
config > ~/gitops/erpnext.yaml
docker compose --project-name erpnext -f ~/gitops/erpnext.yaml pull
docker compose --project-name erpnext -f ~/gitops/erpnext.yaml up -d
docker compose --project-name erpnext exec backend \
bench --site erp.example.com migrate
docker compose --project-name erpnext exec backend \
bench --site erp.example.com set-maintenance-mode offMaintenance mode quan trọng vì migrate thay đổi schema trong khi chạy. Nếu user submit một document vào bảng đang migrate dở, bạn có thể phải sửa record thủ công.
Mỗi lần chỉ chuyển lên một major version, và tạo backup giữa từng bước. Mã migration trong một release được viết để upgrade từ release ngay trước đó, nên bỏ qua major version sẽ chạy các migration theo một tổ hợp chưa được ai kiểm thử.
Repository cũng cung cấp overrides/compose.migrator.yaml, thành phần này thêm một container chạy bench --site all migrate sau mỗi lần start. Cách này tiện. Nhưng nó cũng có nghĩa là một docker compose up với tag đã thay đổi có thể migrate production database mà không có ai giám sát. Với một business system, hãy chạy migrate như một quyết định bạn đã chủ động đưa ra vào sáng hôm đó.
Tăng cường bảo mật cho máy chủ lưu trữ hồ sơ khách hàng
Đổi mật khẩu Administrator ngay lần đăng nhập đầu tiên. File compose dùng để đánh giá cung cấp admin làm mật khẩu đó, và thói quen này có thể theo người dùng vào môi trường production.
Đổi DB_PASSWORD khỏi giá trị 123 trong example.env. Giá trị đó xuất hiện dưới dạng plain text trong ~/gitops/erpnext.yaml đã render, vì vậy hãy chmod 600 file này và không đưa nó vào bất kỳ git repository nào. Nếu cần cách mạnh hơn, overrides/compose.mariadb-secrets.yaml sẽ đọc mật khẩu từ Docker secret file thay vì environment variable. xử lý env file và secret trong Docker Compose trình bày các đánh đổi.
Chỉ publish những gì cần thiết. Với HTTPS override, chỉ các cổng 80 và 443 được expose. Không thêm mapping ports vào service db chỉ để giúp database client kết nối dễ hơn: việc đó sẽ đưa MariaDB lên public internet. Thay vào đó, hãy dùng docker compose --project-name erpnext exec backend bench mariadb. Trên host, chỉ cho phép 22, 80 và 443, từ chối các cổng còn lại, đồng thời kiểm tra cả network firewall riêng của provider.
Bật xác thực hai yếu tố trong System Settings cho mọi account có role System Manager. Role này có thể đọc mọi document và export mọi table, vì vậy hãy coi đây là administrator account thay vì một tiện ích. Nếu bạn chạy nhiều app tự host, Authentik làm nhà cung cấp single sign-on tự host sẽ phù hợp hơn việc dùng thêm một password cho mỗi app.
Patch host và reboot khi có kernel update. Trước khi dựa vào việc stack tự khởi động lại, hãy kiểm tra file đã render để xác nhận policy restart trên từng service, vì stack không có policy này sẽ vẫn dừng sau lần reboot đó. cấu hình Docker Compose stack tự khởi động lại sau reboot trình bày phần systemd.
Khi ERPNext không còn vận hành thoải mái trên một VPS
Một VPS có thể chạy ổn định cho một công ty nhỏ trong thời gian dài. Các dấu hiệu cho thấy máy đã không còn đủ tải:
- Các background job bị dồn lại, nên email và dữ liệu import đến trễ vài phút hoặc vài giờ.
docker inspectbáo cáo các container có"OOMKilled": truehoặc exit code 137.- Báo cáo trước đây chạy mất 2 giây nay mất 30 giây, và MariaDB là tiến trình chiếm CPU.
- Backup chạy lâu đến mức một lần chạy chồng lên lần chạy theo lịch tiếp theo.
Trước tiên, hãy cấp riêng tài nguyên cho MariaDB, vì database và các Python worker đang tranh cùng một vùng memory, trong khi buffer pool là thành phần cần thêm memory. Tăng tài nguyên cho application server thường không hiệu quả như nhiều người nghĩ. chạy database trong Docker hoặc trên host giải thích lựa chọn này, còn đặt giới hạn memory trong Docker Compose giúp ngăn một container làm các container khác cạn tài nguyên trong lúc bạn xử lý.
Sau đó, hãy thêm queue worker thay vì tăng capacity cho web. Các tác vụ chậm của ERPNext chạy ở background, chẳng hạn tạo report và import số lượng lớn. Thêm worker container tốn ít hơn việc nâng cấp máy, đồng thời xử lý đúng vấn đề mà người dùng thực sự phàn nàn.
FAQ
ERPNext cần bao nhiêu RAM trên VPS?
Hướng dẫn chính thức bắt đầu từ 4 GB với 2 vCPU, nhưng cấu hình này chỉ dùng để đánh giá. Với công ty sử dụng hằng ngày, hãy dự kiến 8 GB RAM, 4 vCPU và 100 GB SSD. Nếu thấp hơn, kernel OOM killer sẽ dừng các container khi tải tăng. docker inspect ghi nhận tình trạng này là "OOMKilled": true với exit code 137. Đây chỉ là mức khởi điểm, không phải số đo thực tế. Hãy theo dõi mức sử dụng memory của hệ thống trong tháng đầu tiên.
Có thể chạy pwd.yml trong production không?
Không. README của project mô tả file này chỉ dành cho các lần đánh giá ngắn hạn và lưu ý rằng bạn không thể cài custom app vào đó. Hãy dùng compose.yaml với các override cho MariaDB, Redis và HTTPS, kết xuất chúng thành một file duy nhất bằng docker compose config, rồi chạy file đó.
Vì sao site ERPNext không truy cập được ngay sau khi tôi tạo?
Theo mặc định, frontend chọn site cần phục vụ dựa trên HTTP Host header. Vì vậy, tên site phải khớp với domain trong trình duyệt. Site được tạo với tên erpnext sẽ không được phục vụ tại erp.example.com. Hãy tạo site với domain làm tên site, hoặc đặt FRAPPE_SITE_NAME_HEADER trong file env thành tên site, kết xuất lại compose file rồi restart stack.
Backup ERPNext phải có những gì?
Cần giữ cùng nhau 4 file: dump -database.sql.gz, các archive -files.tar và -private-files.tar, cùng bản sao cấu hình -site_config_backup.json. Chạy bench --site erp.example.com backup --with-files sẽ tạo đủ cả 4 file. Bản sao cấu hình chứa encryption_key. Vì vậy, nếu restore mà không có file này, các password tích hợp đã lưu sẽ không thể giải mã và lỗi sẽ xuất hiện dưới dạng Encryption key is invalid! Please check site_config.json.
Làm cách nào để upgrade ERPNext mà không làm hỏng dữ liệu?
Backup bằng --with-files, bật maintenance mode, thay đổi ERPNEXT_VERSION trong file env, kết xuất lại compose file, pull image, khởi động stack, sau đó chạy bench --site erp.example.com migrate và tắt maintenance mode. Mỗi lần chỉ chuyển một major version và đọc release notes trước, vì migrate sẽ ghi lại schema và dữ liệu document mà không có cách hoàn tác. Rollback nghĩa là restore bản backup đã tạo lúc bắt đầu.