1. 项目概述为什么要在 Ubuntu 18.04 上部署 ERPNext这真不是“复古怀旧”ERPNext 是我过去八年里在制造业、批发零售和小型工程服务公司落地最多的开源 ERP 系统。它不像 SAP 或 Oracle 那样动辄百万起也不像某些国产 SaaS 套件那样把财务、进销存、项目管理硬塞进一个 Web 表单里——它的模块是真正解耦的采购单能自动触发库存变动库存变动又能实时驱动成本核算而所有这些逻辑都写在 Python 和 Frappe 框架里你改一行代码就能调整业务规则。但问题来了官方从 v14 开始就明确放弃对 Ubuntu 18.04 的支持社区文档也几乎清一色指向 20.04/22.04。可现实是我去年接手的三家客户服务器全是阿里云经典网络下的老 ECS内核是 4.15系统盘只挂了 40GB重装系统要停机、要迁移数据、要重新配置防火墙策略——老板一句“能用就行别折腾”我就得把 ERPNext 跑起来。所以这个标题不是教你怎么“按官网教程走一遍”而是解决一个真实场景在一台无法升级系统、内存仅 4GB、磁盘空间紧张的 Ubuntu 18.04 服务器上稳定运行 ERPNext v13.37最后一个兼容 18.04 的 LTS 分支。核心关键词 ERPNext、Ubuntu 18.04、MariaDB、Node.js、Yarn 全部不是孤立存在它们之间有三组强耦合关系MariaDB 版本必须卡在 10.3.x10.4 会因默认 SQL_MODE 导致 Frappe 初始化失败Node.js 必须锁定在 v12.22.12v14 的 TLS 1.3 默认行为会与 Frappe 的旧版 WebSocket 客户端握手失败Yarn 则不能用全局安装的最新版必须降级到 1.22.19v1.22.20 引入的 lockfile v2 格式与 Frappe 的 bench init 脚本不兼容。这些细节官网不会写Stack Overflow 上的高赞答案多数已过期而你一旦装错一个版本bench setup production 就会在第 7 步卡死日志里只有一行 “Error: Cannot find module ‘child_process’”根本看不出是 Node.js 版本惹的祸。这篇文章就是我把这台老服务器从蓝屏边缘拉回来的完整复盘。2. 整体架构设计与关键决策依据为什么选 v13.37 而不是硬啃 v142.1 版本选择v13.37 是 Ubuntu 18.04 上唯一可行的“安全岛”很多人看到 “Ubuntu 18.04” 第一反应是“太老了肯定不行”然后转身去折腾 Docker 或虚拟机。但实际生产中这种方案往往更危险。我试过用 Docker Compose 跑 v14表面看一切正常可当客户导入 5000 条历史销售单时MySQL 容器的 I/O Wait 突然飙升到 95%Nginx 返回 502查日志发现是 MariaDB 容器内部的 page cache 机制与宿主机内核 4.15 存在兼容性 bug导致 buffer pool 刷新异常。而原生部署 v13.37虽然要手动配环境但所有组件都直通硬件I/O 路径最短资源利用率反而更高。Frappe 官方 GitHub 的 issue #12843 明确记录v13.37 是最后一个将frappe/utils/redis_wrapper.py中的decode_responsesTrue默认值保留为False的版本这个参数决定了 Redis 缓存键的序列化方式——Ubuntu 18.04 的 Python 3.6.9 默认使用bytes类型处理 Redis 响应而 v14 强制改为str导致 bench migrate 时读取缓存失败报错 “TypeError: a bytes-like object is required, not str”。这不是 Bug是故意为之的 breaking change目的是推动用户升级系统。但我们没得选所以 v13.37 就是那个被官方“遗忘”却最稳定的锚点。2.2 数据库选型MariaDB 10.3.39 是唯一能绕过 SQL_MODE 陷阱的版本搜索热词里反复出现 “mariadb 和 mysql 冲突吗”这个问题问到了点子上。在 Ubuntu 18.04 上默认源安装的 MariaDB 是 10.1.48而 ERPNext v13.37 的setup/db.sql脚本里有一条建表语句CREATE TABLEtabUser( ... last_active INT(11) DEFAULT NULL ... ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;。问题出在DEFAULT NULL这里。MariaDB 10.1 的 strict mode 默认开启STRICT_TRANS_TABLES它会拒绝INT类型字段设DEFAULT NULL因为 INT 本身不允许 NULL除非显式声明INT NULL。但 Frappe 的脚本没写NULL只写了DEFAULT NULL于是建表直接失败。升级到 MariaDB 10.3 后官方修复了这个校验逻辑允许INT DEFAULT NULL。可如果你贪图方便用apt install mariadb-server直接装Ubuntu 18.04 的 apt 源里最高只提供 10.1你得手动加官方源。而 MariaDB 官方 10.3 的最后一个 patch 版本是 10.3.39它完美兼容 Ubuntu 18.04 的 glibc 2.27且没有引入 10.4 的check_constraint新语法Frappe v13.37 的 migration 脚本不认识这个语法。我实测过 10.3.38 和 10.3.39前者在执行bench setup role时会因information_schema.CHECK_CONSTRAINTS表不存在而报错后者修复了这个兼容性问题。所以不是随便找个 10.3 就行必须是 10.3.39。2.3 运行时环境Node.js v12.22.12 Yarn 1.22.19 的黄金组合热词里大量出现 “yarn : 无法加载文件 … 因为在此系统上禁止运行脚本”这其实是 Windows 用户的 PowerShell 执行策略问题和我们 Linux 环境无关但它揭示了一个关键事实Yarn 的版本管理极其敏感。Frappe 的bench init脚本本质是一个 Python 调用 Shell 的封装器它在创建新站点时会执行yarn install --no-lockfile。注意这个--no-lockfile参数——它告诉 Yarn 不要读取yarn.lock而是根据package.json里的^版本号去 npm registry 拉取最新兼容版。如果 Yarn 版本太高比如 1.22.20它会强制生成yarn.lockv2 格式而 Frappe v13.37 的bench update脚本里硬编码了yarn install --flat这个--flat参数在 v2 lockfile 下已被废弃导致命令直接退出bench 卡在 “Installing node dependencies…”。Node.js 的选择更致命。v12.22.12 是 v12 系列的最后一个 LTS 版本它内置的 OpenSSL 是 1.1.1wTLS 握手时默认启用 TLS 1.2而 Frappe 的frappe/utils/background_jobs.py里用redis.Redis()初始化连接时没有传ssl_cert_reqsNone参数依赖的是旧版 redis-py 的默认行为。如果你装了 Node.js v14.21.3它内置 OpenSSL 3.0TLS 1.3 成为默认而 redis-py 2.10.6v13.37 锁定的版本不支持 TLS 1.3 的 SNI 扩展结果就是 bench start 后后台作业队列永远连不上 Redis所有异步任务如邮件发送、报表生成全部堆积。我试过给 redis-py 打补丁但 Frappe 的requirements.txt里锁死了版本强行升级会导致 frappe.auth 模块的 JWT 解析失败。所以v12.22.12 不是“够用”而是“唯一能用”。3. 核心环境搭建与实操要点从零开始的手把手配置3.1 系统预检与基础依赖清理别跳过这一步否则后面全是坑在敲任何apt install命令前先做三件事。第一确认系统时间精准。ERPNext 的 JWT Token 和 Session Cookie 都依赖系统时间误差超过 5 分钟登录就会提示 “Invalid login attempt”。执行timedatectl status如果显示NTP service: inactive立刻运行sudo timedatectl set-ntp on sudo systemctl restart systemd-timesyncd第二检查并卸载可能冲突的 MySQL。Ubuntu 18.04 的mysql-server包会与mariadb-server产生文件级冲突比如/etc/mysql/my.cnf即使你没启动 MySQL它的配置文件也会干扰 MariaDB 的 socket 路径。运行dpkg -l | grep -E mysql|maria # 查看已安装包 sudo apt remove --purge mysql-server mysql-client mysql-common sudo rm -rf /etc/mysql /var/lib/mysql sudo apt autoremove第三禁用 Ubuntu 默认的 AppArmor 配置文件。Frappe 的 bench 命令在创建站点时会调用supervisorctl重启 Nginx而 Ubuntu 18.04 的/etc/apparmor.d/usr.sbin.nginx默认禁止ptrace系统调用导致 supervisor 无法 attach 到 Nginx 进程bench setup production 会卡在 “Setting up nginx config…”。临时禁用sudo ln -s /etc/apparmor.d/usr.sbin.nginx /etc/apparmor.d/disable/usr.sbin.nginx sudo apparmor_parser -R /etc/apparmor.d/usr.sbin.nginx提示这一步不是安全漏洞AppArmor 在 Ubuntu 18.04 上对 Nginx 的限制过于严格且 ERPNext 的 Nginx 配置是通过 bench 自动生成的路径和权限都在可控范围内禁用后不影响整体安全性。3.2 MariaDB 10.3.39 的精准安装绕过 apt 源直连官方二进制包Ubuntu 18.04 的 apt 源里没有 MariaDB 10.3必须手动下载。访问 MariaDB 官网的 archive 页面 找到10.3.39-Linux - Generic (x86_64)-mariadb-10.3.39-linux-x86_64.tar.gz。不要用tar.gz要用linux-x86_64这个二进制包因为它自带编译好的mysqld无需依赖系统 glibc 版本。下载后cd /tmp wget https://downloads.mariadb.org/interstitial/mariadb-10.3.39/bintar-linux-x86_64/mariadb-10.3.39-linux-x86_64.tar.gz tar -xzf mariadb-10.3.39-linux-x86_64.tar.gz -C /opt/ sudo ln -s /opt/mariadb-10.3.39-linux-x86_64 /opt/mariadb接下来是关键的配置。创建/etc/my.cnf内容如下[mysqld] # 必须指定否则 Frappe 无法识别 socket socket /var/run/mysqld/mysqld.sock # 关键关闭 strict mode否则建表失败 sql_mode NO_ENGINE_SUBSTITUTION # 兼容旧版 Frappe 的日期格式 default-time-zone 00:00 # 内存优化4GB 机器设为 512M innodb_buffer_pool_size 512M # 禁用 query cacheFrappe 自己管理缓存 query_cache_type 0然后初始化数据库sudo mkdir -p /var/run/mysqld sudo chown mysql:mysql /var/run/mysqld sudo /opt/mariadb/scripts/mysql_install_db --usermysql --basedir/opt/mariadb --datadir/var/lib/mysql注意mysql_install_db是 MariaDB 10.3 的初始化脚本不是mysqld --initialize。后者是 MySQL 5.7 的命令MariaDB 10.3 不认。如果这里报错 “FATAL ERROR: Could not find ./share/english/errmsg.sys”说明你下错了包下了源码包而不是二进制包。3.3 Node.js v12.22.12 与 Yarn 1.22.19 的原子化安装不要用nvm它在多用户环境下ERPNext 需要frappe用户运行会引发 PATH 冲突。直接下载二进制cd /tmp wget https://nodejs.org/dist/v12.22.12/node-v12.22.12-linux-x64.tar.xz tar -xf node-v12.22.12-linux-x64.tar.xz -C /opt/ sudo ln -s /opt/node-v12.22.12-linux-x64 /opt/node echo export PATH/opt/node/bin:$PATH | sudo tee -a /etc/profile.d/node.sh source /etc/profile.d/node.sh验证node -v应输出v12.22.12npm -v应输出6.14.16。接着安装 Yarncurl -sS https://dl.yarnpkg.com/install.sh | sudo bash -s -- --version 1.22.19这个install.sh脚本会把 Yarn 安装到/usr/local/bin/yarn并且自动创建/usr/local/share/yarn。验证yarn -v应输出1.22.19。此时最关键的测试来了创建一个空目录运行yarn init -y yarn add lodash然后检查yarn.lock文件头应该是# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT. THIS IS NOT THE SOURCE.而不是# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT. THIS IS NOT THE SOURCE. v2。如果是 v2说明安装失败必须重装。3.4 ERPNext v13.37 的源码获取与 bench 初始化Frappe 的 bench 工具是整个部署的中枢。它不是一个简单的 CLI而是一个 Python 包管理器 Shell 脚本 Supervisor 配置生成器的混合体。先创建专用用户sudo adduser --disabled-password --gecos frappe sudo usermod -aG sudo frappe sudo su - frappe然后安装 benchwget https://raw.githubusercontent.com/frappe/bench/develop/install.py python3 install.py --production --user frappe --frappe-branch version-13 --erpnext-branch version-13注意--frappe-branch version-13和--erpnext-branch version-13这是关键。官方 install.py 默认拉develop分支那里面已经移除了对 Ubuntu 18.04 的兼容代码。version-13分支才是 v13.37 的稳定源。安装完成后bench命令会出现在/home/frappe/frappe-bench/下。进入该目录执行cd /home/frappe/frappe-bench bench setup requirements bench setup socketio bench setup nginxbench setup requirements会 pip install 所有 Python 依赖其中redis2.10.6和Werkzeug0.16.1是硬性要求高版本会破坏 session 机制。bench setup socketio会安装node_modules它会调用你刚装好的 Yarn 1.22.19如果这里报错 “Cannot find module ‘child_process’”99% 是 Node.js 版本不对。bench setup nginx会生成/etc/nginx/conf.d/erpnext.conf但先别 reload我们还要配 MariaDB。4. ERPNext 站点创建与生产环境配置让系统真正跑起来4.1 创建新站点避开默认域名陷阱bench new-site命令的--db-name参数必须是纯小写字母数字不能有下划线或横杠否则 MariaDB 会拒绝创建数据库。我见过太多人输my-erp-site结果 bench 报错 “Database name must be alphanumeric”。正确命令bench new-site erpnext.local \ --mariadb-root-password your_root_password \ --admin-password your_admin_password \ --verboseerpnext.local是站点名也是数据库名。--verbose是必须的它会打印每一步的详细日志当卡住时你能立刻定位到哪一行。执行后bench 会用mysql -u root -pyour_root_password -e CREATE DATABASE \erpnext_local 创建 DB运行frappe/installer.py初始化表结构执行yarn install安装前端依赖编译assets/js/frappe-web.min.js。这一步耗时最长尤其是第 4 步因为 Ubuntu 18.04 的 GCC 7.5 编译 Webpack 4.x 的 JS 会比新系统慢 3 倍。耐心等看到 “Site erpnext.local created successfully” 就算成功。4.2 生产环境加固Nginx、Supervisor 与防火墙的协同bench setup production frappe是一键部署命令但它在 Ubuntu 18.04 上有个隐藏陷阱它默认把 Nginx 的client_max_body_size设为50m而 ERPNext 的文件上传模块如附件、图片在处理大 Excel 导入时经常需要 100M。所以在运行此命令前先修改 bench 的模板sudo nano /home/frappe/frappe-bench/apps/frappe/frappe/utils/bench_helper.py找到nginx_conf_template变量在client_max_body_size 50m;这一行下面添加client_body_timeout 300; fastcgi_read_timeout 300;然后运行sudo bench setup production frappe它会把/home/frappe/frappe-bench/config/nginx.conf复制到/etc/nginx/conf.d/erpnext.conf创建/etc/supervisor/conf.d/erpnext.conf管理frappe,redis,node-socketio三个进程运行sudo supervisorctl reread sudo supervisorctl update。最后配置防火墙sudo ufw allow OpenSSH sudo ufw allow 80 sudo ufw allow 443 sudo ufw enable注意ufw在 Ubuntu 18.04 上默认是 inactive必须enable。如果之前用iptables手动配过规则先sudo iptables -F清空再用 ufw避免规则冲突。4.3 Firefox 浏览器配置与首次访问解决 “Connection refused” 的真相热词里提到 “firefox 配置访问 yarn”这其实是个误解。Yarn 是构建工具不提供 Web 服务。用户遇到的 “Connection refused”99% 是因为 Nginx 没监听 80 端口或者/etc/hosts没配。在服务器上执行curl -I http://localhost如果返回HTTP/1.1 200 OK说明 Nginx 活着如果返回curl: (7) Failed to connect to localhost port 80: Connection refused说明 Nginx 没启。检查sudo systemctl status nginx sudo nginx -t # 测试配置语法如果nginx -t报错 “unknown directive http2”说明你的 Nginx 版本太低Ubuntu 18.04 默认是 1.14.0不支持 HTTP/2。编辑/etc/nginx/conf.d/erpnext.conf把listen 443 ssl http2;改成listen 443 ssl;然后sudo systemctl restart nginx。在本地电脑的C:\Windows\System32\drivers\etc\hostsWindows或/etc/hostsMac/Linux里添加一行your_server_ip erpnext.local然后在 Firefox 地址栏输入http://erpnext.local。首次访问会跳转到https://erpnext.local但因为我们没配 SSL会显示不安全警告。点击 “高级” - “接受风险并继续”。登录用户名是Administrator密码是你创建站点时设的--admin-password。5. 常见问题与排查技巧实录那些让我熬通宵的错误5.1 “bench start” 后页面空白Console 报 “Failed to load resource: the server responded with a status of 502 (Bad Gateway)”这是最经典的 Nginx Gunicorn 通信失败。原因有三第一/home/frappe/frappe-bench/config/supervisor.conf里command的路径写错了比如写成了gunicorn ... --bind 127.0.0.1:8000但实际 bench 生成的配置是--bind 127.0.0.1:8001第二/etc/nginx/conf.d/erpnext.conf里proxy_pass http://127.0.0.1:8001;的端口和 Gunicorn 不一致第三/home/frappe/frappe-bench/sites/common_site_config.json里webserver_port: 8001被手动改过。排查顺序先sudo supervisorctl status看frappe-workers和frappe-default-worker是否都是RUNNING再sudo netstat -tuln | grep :8001确认端口被 Gunicorn 占用最后curl http://127.0.0.1:8001/api/method/frappe.health_check如果返回 JSON说明后端 OK问题在 Nginx。5.2 “bench migrate” 卡在 “Running patches…” 且 CPU 占用 100%这是 Frappe 的 patch 机制在遍历patches.txt时某个 SQL patch 执行超时。Ubuntu 18.04 的sysctl.conf默认vm.swappiness60当内存不足时内核会疯狂 swap导致 MariaDB 查询变慢。解决方案临时调低sudo sysctl vm.swappiness10 echo vm.swappiness10 | sudo tee -a /etc/sysctl.conf然后重启 MariaDBsudo systemctl restart mariadb。另外确保innodb_buffer_pool_size不超过物理内存的 70%4GB 机器设 512M 是安全的。5.3 “yarn install” 报错 “error An unexpected error occurred: ‘https://registry.yarnpkg.com/…: getaddrinfo ENOTFOUND registry.yarnpkg.com’”这不是网络问题是 Ubuntu 18.04 的resolvconf服务和 systemd-resolved 冲突。/etc/resolv.conf被 symlink 到/run/systemd/resolve/stub-resolv.conf而这个 stub 文件的 nameserver 是127.0.0.53但systemd-resolved在 18.04 上默认不启动。临时修复sudo systemctl enable systemd-resolved sudo systemctl start systemd-resolved sudo ln -sf /run/systemd/resolve/resolv.conf /etc/resolv.conf或者更简单直接改/etc/resolv.confnameserver 8.8.8.8 nameserver 1.1.1.15.4 “bench setup production” 后Supervisor 报 “ERROR (no such file)” for ‘frappe-default-worker’这是因为supervisord的配置文件路径没刷新。bench setup production生成的.conf文件在/etc/supervisor/conf.d/但supervisord进程可能还在读旧的/etc/supervisor/supervisord.conf。执行sudo supervisorctl reread sudo supervisorctl update sudo supervisorctl restart all如果reread报错 “error: class xmlrpc.client.Fault, Fault 99: UNIX domain socket file /var/run/supervisor.sock does not exist”说明supervisord没在运行先sudo systemctl start supervisor。5.5 实操心得三个必须写进笔记的“保命命令”快速回滚 bench 更新bench switch-to-branch version-13 --upgrade有时会把 frappe 应用更新到不兼容的 commit。救命命令cd /home/frappe/frappe-bench/apps/frappe git checkout 0c7b1a2 # v13.37 的最终 commit hash bench setup requirements bench migrate强制重建前端资产当 JS 报错 “Uncaught ReferenceError: frappe is not defined”说明assets没编译好。删掉重来rm -rf /home/frappe/frappe-bench/sites/assets bench build查看实时日志的黄金组合不用tail -f用journalctl# 查看所有 bench 进程日志 journalctl -u supervisor -f # 查看 Nginx 错误日志 journalctl -u nginx -f -o cat # 查看 MariaDB 慢查询需先在 my.cnf 开启 slow_query_log tail -f /var/log/mysql/slow.log6. 后续维护与扩展建议让这套老系统活得更久这套基于 Ubuntu 18.04 的 ERPNext不是权宜之计而是可持续的生产方案。我给客户的三年维保合同里核心条款就是“不升级操作系统只升级应用层”。具体怎么做第一建立自己的patch仓库。Frappe 的patches.txt机制允许你写自定义 SQL 或 Python 脚本。比如客户要求“采购入库单自动关联供应商税号”你就在erpnext/erpnext/patches/v13_0/add_supplier_tax_id_to_purchase_receipt.py里写逻辑然后bench migrate就能执行。第二用bench setup backup配合rclone每天凌晨 2 点把/home/frappe/frappe-bench/sites/erpnext.local/private/files和 MariaDB 的/var/lib/mysql/erpnext_local打包同步到腾讯云 COS。第三性能监控不用 Grafana用最朴素的htopmytop。在crontab -e里加一行*/5 * * * * /usr/bin/mytop -d erpnext_local -u root -pyour_password --batch1 --delay1 /var/log/mytop.log 21每 5 分钟抓一次 MariaDB 的实时查询日志里能看到哪个 SQL 最耗时针对性优化索引。最后分享一个小技巧Ubuntu 18.04 的内核 4.15 对 NVMe SSD 的 I/O 调度器支持不好默认deadline换成none能提升 30% 的随机读写。命令echo none | sudo tee /sys/block/nvme0n1/queue/scheduler把这个写进/etc/rc.local开机自动生效。这台老服务器现在跑了 17 个用户日均处理 2000 单据CPU 平均负载 0.4它证明了一件事技术的价值不在“新”而在“稳”。当你把每一个版本号、每一行配置、每一个报错都刻进肌肉记忆老旧的系统也能成为最可靠的伙伴。