SSD Nodes Learn 🎉 VPS $5.50/月起
指南 Matt Connor作者: Matt Connor

如何用 Docker 在 VPS 上自托管 ERPNext

了解 ERPNext Docker VPS 部署的真实要求:11 个容器、内存配置、TLS、外发邮件、版本固定,以及已验证可恢复的备份方案。

您准备运行的内容

在 VPS 上自托管 ERPNext 是一项运维工作,不是执行一条命令即可完成的安装。官方 Docker Compose 堆栈包含 11 个容器,其中保存总账和客户记录。因此,下面所有操作都必须遵循更高标准:备份只有在成功恢复后才算有效备份,未固定版本的镜像标签则可能随时触发架构迁移。

文中会反复出现几个名称。ERPNext 是业务应用程序。Frappe 是其底层的 Python 框架。Bench 是用于管理站点的命令行工具,已安装在容器内部。站点 是一个租户:包含一个 MariaDB 数据库和一个上传文件目录。此处几乎所有命令都会在 backend 容器内,针对一个指定名称的站点运行 bench

本指南使用 frappe_docker 仓库,这是项目维护的部署方案。下面的每条命令都已于 2026 年 8 月针对该仓库完成验证。如果您刚开始使用 Docker Compose,在 VPS 上运行 Docker Compose介绍了本指南所需的基础知识。

ERPNext 需要多大 VPS?

ChartCommon published ERPNext sizing tiers (guidance, not a measurement)
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。另外两个容器 configuratorcreate-site 只执行一次任务,然后退出。这就是容器总数为 11 个的原因。

  • backend 使用 gunicorn 运行 Frappe 应用。bench 位于此处。
  • frontend 是 nginx。它提供静态资源,并将其他所有请求转发到后端。
  • queue-shortqueue-long 是 RQ(Redis Queue)worker。它们运行后台任务,例如发送邮件、导入数据和生成报表。
  • scheduler 执行基于时间的任务,包括计划报表和自动重复文档。
  • websocket 是浏览器实时更新所依赖的 socket.io 进程。
  • db 是 MariaDB。
  • redis-cacheredis-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_PASSWORD123SITES_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 -d

config 不会启动任何服务。它会合并基础文件和覆盖文件,并输出已经替换所有变量的结果。然后运行这个生成的文件。增加这一步是值得的:运行中的堆栈由一个可读取和提交的文件定义,因此即使有人修改 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-apps

list-apps 应输出 frappeerpnext 及其版本。健康的 ps 应在 running 状态下显示 9 个服务,并且没有服务处于 restarting 状态。

这里经常会出现两个问题。使用 Docker 时,--mariadb-user-host-login-scope=% 不是可选项。应用容器通过 Docker 网络连接 MariaDB,因此对 MariaDB 来说它属于远程主机;限定为 localhost 的数据库用户无法从该主机登录。随后,站点创建会失败,并显示 MariaDB access denied 错误,其中会指出 root 用户。% 范围允许新站点的用户从该私有网络中的任意主机访问。

第二个问题是站点名称。默认情况下,前端根据 HTTP Host 标头选择要提供的站点。因此,即使两个站点都存在,使用 erpnext 创建的站点也无法通过 erp.example.com 访问。请像上面一样使用域名作为站点名称,或者在 env 文件中将 FRAPPE_SITE_NAME_HEADER 设置为站点名称,然后再次生成 compose 文件。

HTTPS,以及使其正常工作的前提

compose.https.yaml override 会在端口 443 上运行 Traefik,将端口 80 重定向到端口 443,并从 Let's Encrypt 申请证书。TLS(传输层安全)可避免发票和会话 Cookie 以明文形式在网络中传输。

以下两点必须同时满足,否则永远不会签发证书。erp.example.com 的 DNS A 记录必须已经指向 VPS。端口 80 和 443 必须可从互联网访问,因为 Let's Encrypt 会通过端口 80 上的 HTTP-01 challenge 验证您对该域名的控制权。还要检查服务商的网络防火墙,以及服务器本机上的防火墙。它们是彼此独立的控制项,面板防火墙最容易被忽略。

证书会保存到 cert-data 卷中的 /letsencrypt/acme.json。如果浏览器显示的是默认证书,而不是您的证书,请在 docker compose --project-name erpnext ps 中找到代理服务名称,并查看其日志中的 ACME(自动证书管理环境)错误。要在同一台服务器上运行其他 Web 应用?在多个 Docker Compose 应用前使用一个 Traefik 实例介绍了如何共享代理,而不是争用端口 443。

出站邮件,否则发票永远无法发出

这是大多数 ERPNext 指南都会跳过的步骤,但它决定了系统是否真正可用。没有正常工作的出站邮件,发票无法发送给客户,密码重置邮件无法送达,计划报告也无法交付。该技术栈不包含邮件服务器。

不要尝试让 VPS 直接通过 25 端口发送邮件。大多数服务商会限制新账户的出站 25 端口,而且即使邮件成功发出,也可能被拒收或归入垃圾邮件,因为新 VPS 地址没有发送信誉。请使用经过身份验证的 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(sender policy framework,发件人策略框架)和 DKIM(domainkeys identified mail,域密钥识别邮件)记录,然后添加 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 作为该密码,而这种习惯会被带入生产环境。

example.env 中的 123DB_PASSWORD 更改为其他值。该值会以明文写入渲染后的 ~/gitops/erpnext.yaml,因此请 chmod 600 该文件,并将其排除在任何 git 仓库之外。如需更高强度的方案,overrides/compose.mariadb-secrets.yaml 会从 Docker secret 文件读取密码,而不是从环境变量读取。在 Docker Compose 中处理环境文件和 secret介绍了其中的权衡。

只发布必要的内容。使用 HTTPS 覆盖文件时,仅公开 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。
  • 原本需要两秒的报告现在需要三十秒,并且 MariaDB 进程占用 CPU。
  • 备份运行时间过长,导致一次备份与下一次计划运行重叠。

首先为 MariaDB 分配不与其他服务共享的资源。数据库和 Python worker 会争用同一部分内存,而 buffer pool 往往需要更多内存。升级应用服务器的效果通常不如预期。在 Docker 中或主机上运行数据库介绍了这项选择,在 Docker Compose 中设置内存限制则可在调整期间防止某个容器耗尽其他容器的资源。

之后应增加队列 worker,而不是提升 Web 容量。ERPNext 的慢任务属于后台任务,包括报告生成和批量导入。增加 worker 容器的成本低于升级服务器,而且可以解决用户实际遇到的问题。

FAQ

ERPNext 在 VPS 上需要多少 RAM?

官方发布的指导值从 4 GB RAM 和 2 个 vCPU 起步,但该配置仅适用于评估。公司日常使用时,建议配置 8 GB RAM、4 个 vCPU 和 100 GB SSD。低于此配置时,内核的内存不足终止程序会在负载下停止容器,docker inspect 会将其报告为 "OOMKilled": true,退出代码为 137。这些只是起始配置,不是实际测量结果,因此应在第一个月持续监控实际内存使用情况。

可以在生产环境中运行 pwd.yml 吗?

不可以。项目的 README 说明该文件仅用于短期评估,并指出无法向其中安装自定义应用。请使用带有 MariaDB、Redis 和 HTTPS 覆盖配置的 compose.yaml,再通过 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,最后关闭维护模式。每次只跨越一个主版本,并先阅读发行说明,因为 migrate 会重写架构和文档数据,且无法撤销。回滚意味着还原开始时创建的备份。