Docker VPS 自托管 ERPNext:配置、备份与恢复
了解 ERPNext 的 11 容器 Docker 堆栈、VPS 配置、TLS、邮件、版本固定与恢复验证。1 GB 或 2 GB 内存可能触发 OOM,容器退出码为 137。
您将运行的系统
在 VPS 上自行托管 ERPNext 是一项运维工作,不是执行一条命令即可完成的安装。官方 Docker Compose 堆栈包含 11 个容器,其中保存总账和客户记录。因此,下面所有操作都必须遵循更高标准:备份经过恢复验证后,才算真正的备份;未固定版本的镜像标签,则可能随时触发数据库架构迁移。
文中会反复出现几个名称。ERPNext 是业务应用程序。Frappe 是其底层的 Python 框架。Bench 是用于管理站点的命令行工具,已安装在容器内。站点 是一个租户:包含一个 MariaDB 数据库和一个上传文件目录。本文几乎所有命令都在 backend 容器内通过 bench 针对一个指定站点执行。
本指南使用 frappe_docker 仓库,这是项目维护的部署方案。下面的每条命令都已在 2026 年 8 月针对该仓库进行验证。如果您不熟悉 Docker Compose,在 VPS 上运行 Docker Compose 介绍了本指南所需的基础知识。
ERPNext 需要多大的 VPS?
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
}
]已发布的指导建议是,在第一个用户登录前,至少准备 2 个 vCPU 和 4 GB RAM。这属于评估级配置。这些只是起始参考值,不是本指南测得的数据;实际需求取决于您的文档数量。最后一行根本不是已发布的最低配置,而是内存通常不再成为主要限制的大致配置。
请正视小规格方案的限制。1 GB 或 2 GB 的 VPS 可以启动整套服务,但第一次导入数据或运行第一个耗时较长的报表时就会崩溃。原因是 9 个长期运行的容器、MariaDB 的缓冲池,以及用于生成报表的 Python worker 无法同时放入这么少的内存。故障不会平滑发生。内核的 OOM killer 会停止某个容器,随后对该容器执行 docker inspect 时会显示 "OOMKilled": true,退出码为 137。worker 在任务执行过程中被终止后,已提交的文档可能只完成了一半后台处理。
对于每天使用 ERPNext 的公司,8 GB RAM、4 个 vCPU 和 100 GB SSD 是较为实际的最低配置。通常最先耗尽的是 RAM。磁盘增长速度也会超出预期,因为每个附件和每个本地备份都会写入与数据库相同的卷。
11 个容器及其用途
堆栈启动并运行 9 个容器后,运行 docker compose ps。另外两个容器 configurator 和 create-site 只执行一次任务,然后退出。这就是总数为 11 个的原因。
backend使用 gunicorn 运行 Frappe 应用。bench就在这里运行。frontend是 nginx。它提供静态资源,并将其他请求转发到后端。queue-short和queue-long是 RQ(Redis Queue)worker。它们运行外发邮件、导入和报表生成等后台任务。scheduler执行基于时间的任务,包括计划报表和自动重复文档。websocket是负责浏览器实时更新的 socket.io 进程。db是 MariaDB。redis-cache和redis-queue是两个独立的 Redis 实例,分别用于缓存和任务队列。
了解这种拆分很有用,因为它能告诉您应该查看哪个日志。邮件发送卡住属于队列 worker 问题,因此应运行 docker compose logs -f queue-short。页面可以加载,但通知徽标始终不更新,则属于 websocket 问题。查看这两种问题的 backend 日志都会浪费大量排查时间。
使用生产环境 compose 文件,不要使用演示文件
该仓库附带 pwd.yml,README 对此说明得很直接:“此配置仅适用于短期评估。您无法在此配置中安装自定义应用。”您可以用它花一个下午了解 ERPNext。但不要用它运行公司业务。
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.env打开 ~/gitops/erpnext.env 并修改 4 个值。ERPNEXT_VERSION 固定镜像标签。示例文件中的 DB_PASSWORD 为 123。SITES_RULE 是 Traefik 路由规则,LETSENCRYPT_EMAIL 用于接收证书警告。
ERPNEXT_VERSION=v16.32.1
DB_PASSWORD=<a long random password>
SITES_RULE=Host(`erp.example.com`)
LETSENCRYPT_EMAIL=ops@example.com现在生成一个 compose 文件,然后启动它。
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 不会启动任何服务。它会合并基础文件和覆盖文件,并输出已替换所有变量的结果。然后运行这个生成的文件。增加这一步是有价值的:运行中的栈由一个可读取和提交的文件定义,因此当有人编辑 env 文件或您拉取仓库时,运行配置不会在您不知情的情况下发生变化。多个 Docker Compose 文件如何合并详细说明了覆盖规则。
等待 db 启动并等待 configurator 退出,这需要几秒钟,然后创建站点。
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.com检查结果:
docker compose --project-name erpnext ps
docker compose --project-name erpnext exec backend bench --site erp.example.com list-appslist-apps 应输出 frappe 和 erpnext 及其版本。健康的 ps 应在 running 状态下显示 9 个服务,并且没有服务处于 restarting 状态。
这里经常会出现两个问题。在 Docker 中,--mariadb-user-host-login-scope=% 不是可选项。应用容器通过 Docker 网络连接 MariaDB,因此对数据库来说它属于远程主机;限定为 localhost 的数据库用户无法从该主机登录。随后创建站点会失败,并显示 MariaDB access denied 错误,其中会指出 root 用户。% 作用域允许新站点的用户从该专用网络上的任意主机访问。
第二个问题是站点名称。默认情况下,前端根据 HTTP Host 请求头选择要提供服务的站点,因此,即使两者都存在,以 erpnext 创建的站点也无法通过 erp.example.com 访问。请像上面一样使用域名作为站点名称,或者在 env 文件中将 FRAPPE_SITE_NAME_HEADER 设置为站点名称,然后重新生成 compose 文件。
HTTPS,以及启用前必须满足的条件
compose.https.yaml override 会让 Traefik 监听 443 端口,将 80 端口重定向到 HTTPS,并从 Let's Encrypt 请求证书。TLS(传输层安全)可防止发票和会话 Cookie 以明文形式在网络上传输。
以下两项必须同时满足,否则永远不会签发证书。erp.example.com 的 DNS A 记录必须已经指向 VPS。由于 Let's Encrypt 会通过 80 端口上的 HTTP-01 challenge 验证您是否控制该域名,因此必须能从互联网访问 80 和 443 端口。请同时检查服务商的网络防火墙和服务器上的防火墙。它们是彼此独立的控制项,而面板防火墙最容易被忽略。
证书会保存到 cert-data 卷中的 /letsencrypt/acme.json。如果浏览器显示的是默认证书,而不是您的证书,请在 docker compose --project-name erpnext ps 中找到代理服务名称,并查看其日志中的 ACME(自动证书管理环境)错误。在同一台服务器上运行其他 Web 应用?在多个 Docker Compose 应用前使用一个 Traefik 实例介绍了如何共享代理,避免多个应用争用 443 端口。这类服务器上的第二个应用通常面向客户,而自托管 Chatwoot 客服平台也位于同一代理之后,因此处理发票的人员可以在同一处回复客户的电子邮件和聊天消息。
出站邮件,否则发票永远无法发出
这是大多数 ERPNext 指南会跳过的步骤,但它决定了系统是否真正可用。没有正常工作的出站邮件,发票无法发送给客户,密码重置邮件无法送达,计划报告也不会发送。该软件栈不包含邮件服务器。
不要尝试通过 VPS 的 25 端口直接发送邮件。大多数服务商会阻止新账户的出站 25 端口流量。即使邮件成功发出,也可能被拒收或归入垃圾邮件,因为新 VPS IP 地址没有发信信誉。请使用 587 端口上的身份验证中继。
受支持的方式是在 ERPNext 界面中打开 Email Account 页面。该页面会加密存储密码。也可以将这些键写入站点配置:
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 会将 587 存储为数字,而不是字符串 "587"。重新读取该文件,并确认这两个值没有使用引号:
docker compose --project-name erpnext exec backend \
cat sites/erp.example.com/site_config.json请通过 Email Account 页面设置 mail_password,不要在命令行中设置。这样该值会以加密形式存储,也不会进入 shell 历史记录。
然后发送一封真实邮件。创建 Sales Invoice,将其发送到由您控制的地址,并在发送过程中监控队列:
docker compose --project-name erpnext logs -f queue-short出站邮件作为后台任务运行。因此,邮件未送达时,通常会在该日志中显示为失败任务,而不是在浏览器中显示错误。还应为发信域名发布 SPF(发件人策略框架)和 DKIM(域名密钥识别邮件)记录,然后添加 DMARC 策略。没有这些记录,即使发票本身配置正确,也可能进入客户的垃圾邮件文件夹。如果您希望完全自行管理邮件链路,可以使用 自托管的 Mailcow 邮件服务器,在与 ERP 分开的另一台服务器上运行由您控制的中继。
真正能够恢复的备份
仅有数据库转储并不算 ERPNext 的备份。附件和私有文件位于 sites 目录中,而不在 MariaDB 内。只恢复数据库后,所有已上传的采购订单都会变成失效链接。
docker compose --project-name erpnext exec backend \
bench --site erp.example.com backup --with-files该命令会在 sites 卷的 sites/erp.example.com/private/backups 中写入 4 个文件:
-database.sql.gz转储文件-files.tar公共文件归档-private-files.tar私有文件归档-site_config_backup.json站点配置副本
第 4 个文件最容易被丢弃,但它造成的影响最大。该文件包含 encryption_key,这是 Frappe 用于加密已存储密码的密钥,包括电子邮件账户凭据、支付网关密钥以及所有集成密钥。恢复数据库时如果没有匹配的密钥,站点可以正常加载,但发送邮件会失败,并显示:
frappe.exceptions.ValidationError: Encryption key is invalid! Please check site_config.json始终将这 4 个文件放在一起。
然后将它们移出服务器。卷内的备份无法应对服务器故障,而且 bench 也会清理这些备份:默认情况下,它会删除该目录中超过 24 小时的备份。
docker compose --project-name erpnext cp \
backend:/home/frappe/frappe-bench/sites/erp.example.com/private/backups \
~/erpnext-backups在 cron 中运行该命令,然后将目录推送到不由您管理的位置。使用加密的 restic 备份将数据存储到异地是合适的方案,因为它会在上传前加密数据,而 restic check 可验证存储库仍然可以读取。ERP 备份是整个账本的副本,因此应以静态加密形式存储在另一台硬件上。
在需要恢复之前先测试恢复流程
未经测试的备份只是猜测。将其恢复到同一台服务器上的第二个站点,绝不要恢复到生产站点。
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>'将备份配置中的加密密钥复制到恢复的站点,否则其集成仍会无法正常工作:
docker compose --project-name erpnext exec backend \
bench --site restore-test.example.com set-config encryption_key '<value from site_config_backup.json>'现在像会计人员一样检查恢复结果。打开应收账款报表,并将期末余额与生产站点进行比较。打开最近的一张采购发票,并下载其附件。站点能够显示登录页,完全不能证明恢复成功。
完成后删除测试站点:
docker compose --project-name erpnext exec backend \
bench drop-site restore-test.example.com为什么 ERPNext 更需要固定版本
对于静态网站,未固定的镜像标签可能只会导致意外重启。对于 ERPNext,这意味着执行数据库架构迁移。bench migrate 会重写数据库表,也可能重写文档数据,而且无法撤销。回滚需要从备份恢复,而不是执行 docker compose down。
因此请固定镜像标签。2026 年 8 月,仓库自己的 pwd.yml 中固定的版本是 ERPNEXT_VERSION=v16.32.1。不要在未检查的情况下继续使用这个版本号。当前版本列在 frappe/erpnext 版本发布页面,现有镜像标签列在 Docker Hub。升级前,请阅读目标版本的说明。
升级应从备份和维护模式开始。
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 on编辑 ~/gitops/erpnext.env 中的 ERPNEXT_VERSION,然后渲染配置、拉取镜像并执行迁移。
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 off维护模式很重要,因为 migrate 运行时会修改数据库架构。用户在表只完成部分迁移时提交文档,最终就需要手动修复记录。
每次只升级一个主版本,并在每一步之间创建备份。某个版本中的迁移代码通常只针对从前一个版本升级的场景编写,因此跳过主版本会以未经测试的组合运行迁移。
该仓库还提供 overrides/compose.migrator.yaml,它会添加一个容器,在每次启动时运行 bench --site all migrate。这很方便。但这也意味着,标签发生变化的 docker compose up 可能会在无人监控的情况下迁移生产数据库。在业务系统中,应将执行 migrate 作为当天明确作出的操作决定。
加固存储客户记录的服务器
首次登录时更改 Administrator 密码。评估用 compose 文件将 admin 作为该密码,人们也会把这种习惯带到生产环境中。
将 DB_PASSWORD 更改为不同于 123 的值,该值位于 example.env 中。该值会以明文写入渲染后的 ~/gitops/erpnext.yaml,因此请 chmod 600 该文件,并将其排除在所有 git 仓库之外。如需更高强度的方案,overrides/compose.mariadb-secrets.yaml 会从 Docker secret 文件读取密码,而不是从环境变量读取。在 Docker Compose 中管理环境文件和 secret 介绍了其中的取舍。
只发布必需的端口。使用 HTTPS override 时,只有 80 和 443 端口对外暴露。不要向 db 服务添加 ports 映射来方便数据库客户端连接:这会将 MariaDB 暴露到公网。请改用 docker compose --project-name erpnext exec backend bench mariadb。在主机上允许 22、80 和 443,拒绝其余端口,并同时检查云服务商单独的网络防火墙。
为所有持有 System Manager 角色的账户,在 System Settings 中启用双因素身份验证。该角色可以读取所有文档并导出所有表,因此应将其视为管理员账户,而不是便利功能。如果运行多个自托管应用,使用 Authentik 作为自托管单点登录提供商 比为每个应用再设置一个密码更好。
为主机安装补丁,并在内核更新后重启。在确认该堆栈能够自动恢复前,检查渲染后的文件,确认每个服务都有 restart 策略;没有该策略的堆栈会在重启后保持停止状态。让 Docker Compose 堆栈在重启后再次启动 介绍了 systemd 相关配置。
当 ERPNext 在一台 VPS 上运行不再得心应手时
一台 VPS 可以长期承载一家小型公司的业务。出现以下迹象时,说明它的容量已经不足:
- 后台任务不断堆积,邮件和导入任务延迟数分钟甚至数小时。
docker inspect报告容器出现"OOMKilled": true或退出代码 137。- 原本需要 2 秒的报表现在需要 30 秒,而占用 CPU 的进程是 MariaDB。
- 备份运行时间过长,以至于一次备份会与下一次计划任务重叠。
首先,为 MariaDB 提供不与其他服务共享的资源。数据库和 Python worker 会争用同一份内存,而 buffer pool 往往需要更多内存。单纯扩大应用服务器的规格,效果通常不如预期。在 Docker 中或主机上运行数据库介绍了这项选择;在调整期间,在 Docker Compose 中设置内存限制可以避免某个容器耗尽其他容器的资源。
然后增加队列 worker,而不是扩充 Web 容量。ERPNext 的慢任务属于后台任务,包括报表生成和批量导入。增加 worker 容器的成本低于升级整台服务器,而且可以解决用户实际抱怨的问题。
FAQ
VPS 需要多少 RAM 才能运行 ERPNext?
官方发布的建议从 4 GB RAM 和 2 个 vCPU 起步,但该配置仅适用于评估。企业每天使用时,应配置 8 GB RAM、4 个 vCPU 和 100 GB SSD。低于此配置时,内核的 out of memory killer 会在负载过高时停止容器,docker inspect 会将其报告为 "OOMKilled": true,退出码为 137。这些只是起始配置,不是实际测量结果,因此应在第一个月持续监控自身的内存使用情况。
可以在生产环境中运行 pwd.yml 吗?
不可以。项目的 README 说明该文件仅用于短期评估,并指出无法在其中安装自定义应用。请使用 compose.yaml 及 MariaDB、Redis 和 HTTPS 覆盖配置,再通过 docker compose config 将它们渲染为一个文件,然后运行该文件。
为什么我创建 ERPNext 站点后,立即无法访问?
默认情况下,前端根据 HTTP Host 请求头选择要提供服务的站点,因此站点名称必须与浏览器中的域名匹配。使用 erpnext 创建的站点不会在 erp.example.com 提供服务。请使用域名作为站点名称创建站点,或者在 env 文件中将 FRAPPE_SITE_NAME_HEADER 设置为站点名称,然后重新渲染 compose 文件并重启堆栈。
ERPNext 备份必须包含哪些内容?
需要将以下 4 个文件放在一起:-database.sql.gz 转储文件、-files.tar 和 -private-files.tar 归档文件,以及 -site_config_backup.json 配置副本。运行 bench --site erp.example.com backup --with-files 会生成全部 4 个文件。配置副本包含 encryption_key,因此还原时如果缺少该文件,已存储的集成密码将无法解密,并显示为 Encryption key is invalid! Please check site_config.json。
如何升级 ERPNext,同时避免损坏数据?
使用 --with-files 进行备份,启用维护模式,在 env 文件中修改 ERPNEXT_VERSION,重新渲染 compose 文件,拉取镜像并启动堆栈,然后运行 bench --site erp.example.com migrate,最后关闭维护模式。每次只跨越 1 个主版本,并在升级前阅读发行说明,因为 migrate 会重写架构和文档数据,且无法撤销。回滚意味着还原开始时创建的备份。