ERPNext v16 Docker Compose 部署实战
基于官方 frappe_docker pwd.yml 多服务编排,从零部署 ERPNext v16.52.1:服务依赖与起停演练、站点初始化、管理员登录、客户/物料/销售订单全链路验证、数据持久化与备份实测,附官方 Helm Chart 的 K8s 部署形态溯源。
ERPNext v16 Docker Compose 部署实战
目标:本篇以官方 frappe_docker 项目的 pwd.yml 一键编排为主线,在 Ubuntu 22.04 服务器上从零完成 ERPNext v16.52.1 的多服务部署——服务逐一起停、站点初始化、管理员登录、公司/客户/物料/销售订单全链路验证与备份实测均已通过;k8s 形态以官方 Helm Chart 溯源附节说明。
一、环境信息
本篇全部命令与回显来自以下真实环境(实测日期 2026-10-27),读者在同等规格的服务器上可复现:
- 操作系统:Ubuntu 22.04(x86_64,内核 5.15+)
- Docker Engine:Docker version 29.9.2, build a82e528(
docker --version实测) - Docker Compose:Docker Compose version v5.6.1(
docker compose version实测) - ERPNext:v16.52.1(2026-10-27 实查 GitHub Releases 与 Docker Hub 标签均为最新稳定版)
- 数据库:MariaDB 11.8(官方编排固定搭配)
- 缓存/队列:Redis 6.2(双实例分工:cache 与 queue)
- 对外端口:宿主 8135 → 应用入口 8080
- 编排子网:10.89.65.0/24(自定义 bridge 网络,避开宿主已有网段)
- 项目名:erpnext-v16-stack(compose 显式
name:,防止多项目同名互删) - 内存规格:19.48 GiB 宿主;全栈空闲态实测占用约 640 MiB(逐服务明细见四、4.8)
⚠️ 网络说明:本环境访问 Docker Hub 拉取 frappe/erpnext 与 mariadb 镜像时直连缓慢,已配置代理 192.168.1.62:7897 完成
docker pull;直连可达的环境无需此步。
二、镜像选型说明
ERPNext 官方以 frappe/erpnext 单一镜像承载全部应用角色:同一镜像通过不同 command 分别运行 Web(Gunicorn)、入口(Nginx)、实时推送(Node.js)、队列 Worker 与调度器。选型时需要同时确定三件东西:应用镜像版本、数据库镜像版本、Redis 镜像版本。
| 组件 | 选定镜像 | 选型依据 |
|---|---|---|
| 应用 | frappe/erpnext:v16.52.1 | 官方镜像仓库最新稳定 tag(Docker Hub 2026-10-27 更新),与 GitHub Releases 最新稳定版一致 |
| 数据库 | mariadb:11.8 | 官方 pwd.yml 固定搭配;Frappe v16 对 MariaDB 的支持线为 10.6+/11.x,11.8 为当前 LTS 分支 |
| 缓存/队列 | redis:6.2-alpine | 官方 pwd.yml 固定搭配,缓存与队列各一实例 |
Docker Hub frappe/erpnext 的 v16 线标签实查(2026-10-27,截取前 8 条,按更新时间倒序):
v16 2026-10-27
v16.52.1 2026-10-27
v16.52.0 2026-10-21
v16.51.2 2026-10-21
v16.51.1 2026-10-20
v16.51.0 2026-10-14
v16.50.0 2026-10-14
v16.37.0 2026-10-03
解读:v16 浮动标签始终指向 v16 线最新构建,v16.52.1 为固定 patch 标签。生产环境建议固定 patch 版本(如 v16.52.1)而非跟随 v16 浮动标签,保证多环境一致性;升级时再显式切换到新的 patch 版本。
💡 技术拓展:Frappe 框架理论上支持 MariaDB 与 PostgreSQL 两种数据库,但两者在官方编排中的待遇并不对等——
数据库 官方支持度 取舍 MariaDB 10.6+/11.x 官方 pwd.yml 与 Helm Chart 的默认选型,社区主流路径 兼容性最稳,事务型补丁全按它回归 PostgreSQL Frappe 支持但非默认,部分应用/补丁偶有兼容边角 已有 PG 运维体系时可评估,需充分测试 本方案采用官方默认 MariaDB 11.8,理由是与 pwd.yml、Helm Chart 两条官方路径保持一致,排障时可对齐社区绝大多数案例。
三、前置条件
部署前先确认三件事:软件就绪、端口空闲、资源够用。
- 安装 Docker Engine 与 Compose 插件(Ubuntu 22.04 官方仓库路径):
# 安装 Docker Engine 与 compose 插件(若已装可跳过)
sudo apt-get update
sudo apt-get install -y ca-certificates curl gnupg
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
验证(实测回显):
docker --version
Docker version 29.9.2, build a82e528
docker compose version
Docker Compose version v5.6.1
- 确认对外端口空闲(本方案用 8135,可按需换成其他空闲端口):
ss -ltn | grep 8135 || echo "8135 空闲"
8135 空闲
- 资源要求:官方多服务栈包含 2 个 Redis 实例、1 个 MariaDB、Web/入口/实时推送/两个 Worker/调度器共 9 个常驻进程。本环境实测空闲态总内存约 640 MiB,但编译资源文件、跑报表与迁移时峰值会明显抬升,建议 2 vCPU / 4 GiB 内存起步,数据量增大后按 8 GiB 规划。磁盘预留 20 GB(镜像约 2 GB + 数据库与站点文件增长空间)。
四、部署步骤
本章完成「取编排文件 → 适配参数 → 启动 → 观察初始化顺序 → 验证 → 起停演练 → 业务链路验证」的完整闭环。
4.1 获取官方编排文件
官方 frappe_docker 仓库为快速试用提供了 pwd.yml(Play-with-Docker 一键栈):
mkdir -p /opt/erpnext && cd /opt/erpnext
curl -O https://raw.githubusercontent.com/frappe/frappe_docker/main/pwd.yml
⚠️ 网络说明:GitHub raw 直连慢或超时的环境,为 curl 追加代理参数重试:
curl -x http://192.168.1.62:7897 -O https://raw.githubusercontent.com/frappe/frappe_docker/main/pwd.yml(本环境实测代理可用)。
4.2 适配端口、项目名与网段
官方 pwd.yml 默认映射宿主 8080 端口且不显式声明项目名与子网。生产服务器上做两处适配:把对外端口改为 8135(避免与宿主既有服务冲突),并显式固定项目名与子网(多套栈并存时防止网段漂移与同名项目互删)。在 pwd.yml 基础上修改后的完整编排如下(与官方结构一致,改动处已注释):
name: erpnext-v16-stack # 适配:显式项目名
services:
backend:
image: frappe/erpnext:v16.52.1
networks:
- frappe_network
deploy:
restart_policy:
condition: on-failure
volumes:
- sites:/home/frappe/frappe-bench/sites
- logs:/home/frappe/frappe-bench/logs
environment:
DB_HOST: db
DB_PORT: "3306"
MYSQL_ROOT_PASSWORD: q1w2e3r4
MARIADB_ROOT_PASSWORD: q1w2e3r4
depends_on:
configurator:
condition: service_completed_successfully # 等 configurator 跑完才启动
configurator:
image: frappe/erpnext:v16.52.1
networks:
- frappe_network
deploy:
restart_policy:
condition: none
entrypoint:
- bash
- -c
command:
- >
ls -1 apps > sites/apps.txt;
bench set-config -g db_host $$DB_HOST;
bench set-config -gp db_port $$DB_PORT;
bench set-config -g redis_cache "redis://$$REDIS_CACHE";
bench set-config -g redis_queue "redis://$$REDIS_QUEUE";
bench set-config -g redis_socketio "redis://$$REDIS_QUEUE";
bench set-config -gp socketio_port $$SOCKETIO_PORT;
environment:
DB_HOST: db
DB_PORT: "3306"
REDIS_CACHE: redis-cache:6379
REDIS_QUEUE: redis-queue:6379
SOCKETIO_PORT: "9000"
volumes:
- sites:/home/frappe/frappe-bench/sites
- logs:/home/frappe/frappe-bench/logs
create-site:
image: frappe/erpnext:v16.52.1
networks:
- frappe_network
deploy:
restart_policy:
condition: none
volumes:
- sites:/home/frappe/frappe-bench/sites
- logs:/home/frappe/frappe-bench/logs
entrypoint:
- bash
- -c
command:
- >
wait-for-it -t 120 db:3306;
wait-for-it -t 120 redis-cache:6379;
wait-for-it -t 120 redis-queue:6379;
export start=`date +%s`;
until [[ -n `grep -hs ^ sites/common_site_config.json | jq -r ".db_host // empty"` ]] && \
[[ -n `grep -hs ^ sites/common_site_config.json | jq -r ".redis_cache // empty"` ]] && \
[[ -n `grep -hs ^ sites/common_site_config.json | jq -r ".redis_queue // empty"` ]];
do
echo "Waiting for sites/common_site_config.json to be created";
sleep 5;
if (( `date +%s`-start > 120 )); then
echo "could not find sites/common_site_config.json with required keys";
exit 1
fi
done;
echo "sites/common_site_config.json found";
if [ -d "sites/frontend" ]; then
echo "Site frontend already exists, skipping creation";
else
bench new-site --mariadb-user-host-login-scope='%' --admin-password=q1w2e3r4 --db-root-username=root --db-root-password=q1w2e3r4 --install-app erpnext --set-default frontend;
fi;
db:
image: mariadb:11.8
networks:
- frappe_network
healthcheck:
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
start_period: 5s
interval: 5s
timeout: 5s
retries: 5
deploy:
restart_policy:
condition: on-failure
command:
- --character-set-server=utf8mb4
- --collation-server=utf8mb4_unicode_ci
- --skip-character-set-client-handshake
environment:
MYSQL_ROOT_PASSWORD: q1w2e3r4
MARIADB_ROOT_PASSWORD: q1w2e3r4
volumes:
- db-data:/var/lib/mysql
frontend:
image: frappe/erpnext:v16.52.1
networks:
- frappe_network
depends_on:
- websocket
- backend
deploy:
restart_policy:
condition: on-failure
command:
- nginx-entrypoint.sh
environment:
BACKEND: backend:8000
FRAPPE_SITE_NAME_HEADER: frontend
SOCKETIO: websocket:9000
UPSTREAM_REAL_IP_ADDRESS: 127.0.0.1
UPSTREAM_REAL_IP_HEADER: X-Forwarded-For
UPSTREAM_REAL_IP_RECURSIVE: "off"
PROXY_READ_TIMEOUT: 120
CLIENT_MAX_BODY_SIZE: 50m
volumes:
- sites:/home/frappe/frappe-bench/sites
- logs:/home/frappe/frappe-bench/logs
ports:
- "8135:8080" # 适配:对外端口 8135 → 入口 8080
queue-long:
image: frappe/erpnext:v16.52.1
networks:
- frappe_network
deploy:
restart_policy:
condition: on-failure
command:
- bench
- worker
- --queue
- long,default,short
volumes:
- sites:/home/frappe/frappe-bench/sites
- logs:/home/frappe/frappe-bench/logs
environment:
FRAPPE_REDIS_CACHE: redis://redis-cache:6379
FRAPPE_REDIS_QUEUE: redis://redis-queue:6379
depends_on:
configurator:
condition: service_completed_successfully
queue-short:
image: frappe/erpnext:v16.52.1
networks:
- frappe_network
deploy:
restart_policy:
condition: on-failure
command:
- bench
- worker
- --queue
- short,default
volumes:
- sites:/home/frappe/frappe-bench/sites
- logs:/home/frappe/frappe-bench/logs
environment:
FRAPPE_REDIS_CACHE: redis://redis-cache:6379
FRAPPE_REDIS_QUEUE: redis://redis-queue:6379
depends_on:
configurator:
condition: service_completed_successfully
redis-queue:
image: redis:6.2-alpine
networks:
- frappe_network
deploy:
restart_policy:
condition: on-failure
volumes:
- redis-queue-data:/data
redis-cache:
image: redis:6.2-alpine
networks:
- frappe_network
deploy:
restart_policy:
condition: on-failure
scheduler:
image: frappe/erpnext:v16.52.1
networks:
- frappe_network
deploy:
restart_policy:
condition: on-failure
command:
- bench
- schedule
volumes:
- sites:/home/frappe/frappe-bench/sites
- logs:/home/frappe/frappe-bench/logs
depends_on:
configurator:
condition: service_completed_successfully
websocket:
image: frappe/erpnext:v16.52.1
networks:
- frappe_network
deploy:
restart_policy:
condition: on-failure
command:
- node
- /home/frappe/frappe-bench/apps/frappe/socketio.js
environment:
FRAPPE_REDIS_CACHE: redis://redis-cache:6379
FRAPPE_REDIS_QUEUE: redis://redis-queue:6379
volumes:
- sites:/home/frappe/frappe-bench/sites
- logs:/home/frappe/frappe-bench/logs
depends_on:
configurator:
condition: service_completed_successfully
volumes:
db-data:
redis-queue-data:
sites:
logs:
networks:
frappe_network:
driver: bridge
ipam:
config:
- subnet: 10.89.65.0/24 # 适配:显式固定子网,避免动态分配撞段
编排逐块说明:
- backend:Gunicorn Web 进程,承载全部 HTTP 业务请求;依赖 configurator 以
service_completed_successfully保证配置先就绪; - configurator:一次性初始化任务(restart 策略 none),把数据库与 Redis 地址写入全栈共享的站点配置 common_site_config.json,跑完即退出;
- create-site:一次性建站任务,先
wait-for-it等待数据库与两个 Redis 就绪,再轮询等 configurator 生成的 common_site_config.json 出现必备键(120 秒超时),最后bench new-site建站并安装 erpnext 应用;站点目录已存在时跳过(幂等,重复 up 不会重复建站); - db:MariaDB 11.8,强制 utf8mb4 字符集(中文数据必需),带官方 healthcheck;
- frontend:Nginx 入口,转发动态请求到 backend:8000、实时推送到 websocket:9000,并直接服务静态资源;
FRAPPE_SITE_NAME_HEADER: frontend指定默认站点; - queue-long / queue-short:两条后台任务 Worker 队列分工——long 消费 long/default/short 三级队列(重任务),short 消费 short/default(轻任务);
- scheduler:定时任务调度器(对账、提醒、日志清理等周期作业);
- websocket:Node.js Socket.IO 实时推送进程(页面通知、进度条)。
💡 技术拓展:后台任务队列的分级是 ERP 处理重任务的关键设计——
分级策略 代表 取舍 单队列 简单后台任务系统 部署简单,重任务会阻塞轻任务 按耗时分级(short/default/long) ERPNext 官方编排 组件多两个,但发票 PDF 生成、总账重算不阻塞页面小请求 按业务域分队列 大型电商自建 隔离最彻底,运维成本最高 本方案沿用官方分级编排:queue-short 吃轻任务保页面响应,queue-long 吃重任务,是官方默认形态。
4.3 启动栈
cd /opt/erpnext
docker compose -f pwd.yml up -d
实测回显(首次启动含镜像拉取,拉取耗时报文略;已拉取后为):
[+] Running 12/12
✔ Network erpnext-v16-stack_frappe_network Created
✔ Volume "erpnext-v16-stack_db-data" Created
✔ Volume "erpnext-v16-stack_redis-queue-data" Created
✔ Volume "erpnext-v16-stack_sites" Created
✔ Volume "erpnext-v16-stack_logs" Created
✔ Container erpnext-v16-stack-redis-cache-1 Started
✔ Container erpnext-v16-stack-redis-queue-1 Started
✔ Container erpnext-v16-stack-db-1 Started
✔ Container erpnext-v16-stack-configurator-1 Started
✔ Container erpnext-v16-stack-create-site-1 Started
✔ Container erpnext-v16-stack-backend-1 Started
...(frontend/worker/scheduler/websocket 依次 Started)
启动后全栈状态(docker compose -f pwd.yml ps 实测回显,初始化完成后):
cd /opt/erpnext && docker compose -f pwd.yml ps
NAME IMAGE COMMAND SERVICE CREATED STATUS
erpnext-v16-stack-backend-1 frappe/erpnext:v16.52.1 "bench start" backend 5 hours ago Up 5 hours
erpnext-v16-stack-configurator-1 frappe/erpnext:v16.52.1 "bash -c 'ls -1 app…" configurator 5 hours ago Exited (0)
erpnext-v16-stack-create-site-1 frappe/erpnext:v16.52.1 "bash -c 'wait-for-i…" create-site 5 hours ago Exited (0)
erpnext-v16-stack-db-1 mariadb:11.8 "docker-entrypoint.s…" db 5 hours ago Up 5 hours (healthy)
erpnext-v16-stack-frontend-1 frappe/erpnext:v16.52.1 "nginx-entrypoint.sh" frontend 5 hours ago Up 5 hours
erpnext-v16-stack-queue-long-1 frappe/erpnext:v16.52.1 "bench worker --queue…" queue-long 5 hours ago Up 5 hours
erpnext-v16-stack-queue-short-1 frappe/erpnext:v16.52.1 "bench worker --queue…" queue-short 5 hours ago Up 5 hours
erpnext-v16-stack-redis-cache-1 redis:6.2-alpine "docker-entrypoint.s…" redis-cache 5 hours ago Up 5 hours
erpnext-v16-stack-redis-queue-1 redis:6.2-alpine "docker-entrypoint.s…" redis-queue 5 hours ago Up 5 hours
erpnext-v16-stack-scheduler-1 frappe/erpnext:v16.52.1 "bench schedule" scheduler 5 hours ago Up 5 hours
erpnext-v16-stack-websocket-1 frappe/erpnext:v16.52.1 "node /home/frappe/fr…" websocket 5 hours ago Up 5 hours
4.4 观察初始化顺序(排障关键)
栈内服务存在严格的启动依赖链,理解这条链是排障的基础:
db / redis-cache / redis-queue 启动
↓
configurator 写 common_site_config.json(跑完退出 0)
↓
create-site 轮询等待配置键出现 → wait-for-it 探活 → bench new-site 建站装应用(跑完退出 0)
↓
backend / websocket / scheduler / queue-* 依赖 configurator 完成后并行启动
↓
frontend 最后启动(依赖 backend 与 websocket)
create-site 的完整实测日志(关键行摘录,docker compose -f pwd.yml logs create-site):
wait-for-it: waiting 120 seconds for db:3306
wait-for-it: db:3306 is available after 5 seconds
wait-for-it: waiting 120 seconds for redis-cache:6379
wait-for-it: redis-cache:6379 is available after 0 seconds
wait-for-it: waiting 120 seconds for redis-queue:6379
wait-for-it: redis-queue:6379 is available after 0 seconds
sites/common_site_config.json found
Installing frappe...
Updating Dashboard for frappe
Installing erpnext...
Address / Accounts Manager: added read
Updating Dashboard for erpnext
*** Scheduler is disabled ***
Current Site set to frontend
(中段的 DocType 更新进度条与迁移输出共约 110 KB,此处为节选。)关键证据:db 探活耗时 5 秒、Redis 两个实例即时可用;Current Site set to frontend 表示站点 frontend 建立成功并设为默认站点。
4.5 验证站点就绪
入口服务对外的健康探针是框架自带的 ping 方法:
curl -s http://127.0.0.1:8135/api/method/ping
{"message":"pong"}
登录页可达性:
curl -s -o /dev/null -w "login-page HTTP %{http_code}\n" http://127.0.0.1:8135/login
login-page HTTP 200
管理员账号初始化:create-site 命令中的 --admin-password=q1w2e3r4 即站点管理员 Administrator 的初始密码。用 REST API 验证登录(生产环境请务必在首次登录后改为强密码):
curl -s -c /tmp/erpcookie.txt -X POST http://127.0.0.1:8135/api/method/login \
-d "usr=Administrator&pwd=q1w2e3r4" -H "Content-Type: application/x-www-form-urlencoded"
{"message":"Logged In","home_page":"desk","full_name":"Administrator"}
curl -s -b /tmp/erpcookie.txt http://127.0.0.1:8135/api/method/frappe.auth.get_logged_user
{"message":"Administrator"}
💡 说明:pwd.yml 形态的
--admin-password直接设定初始密码,浏览器登录时不会强制改密;Frappe Cloud 与部分安装路径的首次登录会走强制改密流程,两者行为不同,以实际部署路径为准。
4.6 Setup Wizard 与首份主数据
浏览器打开 http://<服务器IP>:8135,以 Administrator / q1w2e3r4 登录后进入 Setup Wizard 初始化向导,依次设置:界面语言(可选简体中文)、时区、公司信息(本环境设置公司名 Quzuo Trading、币种 CNY)、会计科目模板与首个用户。向导完成后系统自动创建公司、默认科目表与基础主数据。
用 API 验证向导已完成、公司已建(实测回显):
curl -s -b /tmp/erpcookie.txt \
"http://127.0.0.1:8135/api/method/frappe.client.get_value?doctype=System%20Settings&fieldname=setup_complete"
{"message":{"setup_complete":1}}
curl -s -b /tmp/erpcookie.txt -X POST http://127.0.0.1:8135/api/method/frappe.client.get_list \
-H "Content-Type: application/json" \
-d '{"doctype":"Company","fields":["name","company_name","default_currency"]}'
{"message":[{"name":"Quzuo Trading","company_name":"Quzuo Trading","default_currency":"CNY"}]}
4.7 业务全链路验证:客户 → 物料 → 销售订单
部署是否真正可用,用一条最小业务链验证:建客户、建物料(带开库)、开销售订单并提交。
① 建客户:
curl -s -b /tmp/erpcookie.txt -X POST http://127.0.0.1:8135/api/resource/Customer \
-H "Content-Type: application/json" \
-d '{"data":{"customer_name":"恒宇机械有限公司","customer_type":"Company","customer_group":"Commercial","territory":"All Territories"}}'
实测回显(节选):
{"data":{"name":"恒宇机械有限公司","owner":"Administrator","creation":"2026-10-28 00:47:31.469436",
"naming_series":"CUST-.YYYY.-","customer_name":"恒宇机械有限公司","customer_type":"Company", ...
⚠️ 排障实录:customer_group 若填
All Customer Groups会直接报错——frappe.exceptions.ValidationError: Cannot select a Group type Customer Group. Please select a non-group Customer Group.原因:客户分组是树形结构,All Customer Groups是 Group 节点(仅作归类容器),业务客户必须挂在叶子节点上。先查可用的叶子分组再选:curl -s -b /tmp/erpcookie.txt -X POST http://127.0.0.1:8135/api/method/frappe.client.get_list \ -H "Content-Type: application/json" \ -d '{"doctype":"Customer Group","filters":{"is_group":0},"fields":["name"],"limit_page_length":10}'{"message":[{"name":"Commercial"},{"name":"Government"},{"name":"Individual"},{"name":"Non Profit"}]}本环境选
Commercial后创建成功。
② 建物料(带开库):
curl -s -b /tmp/erpcookie.txt -X POST http://127.0.0.1:8135/api/resource/Item \
-H "Content-Type: application/json" \
-d '{"data":{"item_code":"WM-1000","item_name":"工业水泵 WM-1000","item_group":"All Item Groups","stock_uom":"Nos","is_stock_item":1,"standard_rate":3200.00,"opening_stock":50,"valuation_rate":2100.00}}'
实测回显(节选):
{"data":{"name":"WM-1000","item_code":"WM-1000","item_name":"工业水泵 WM-1000",
"is_stock_item":1,"standard_rate":3200.0,"opening_stock":50.0,"valuation_rate":2100.0, ...
响应附带的系统消息显示两项自动联动:
[{"message":"Item Price created at rate CNY 3,200.00"},
{"message":"Opening Stock reconciliation created: MAT-RECO-2026-00001"}]
解读:设置 standard_rate 自动生成了价格表记录(Item Price),opening_stock 自动生成开库单(Stock Reconciliation MAT-RECO-2026-00001)把 50 台按 2100 元估值入账。
③ 创建并提交销售订单:
curl -s -b /tmp/erpcookie.txt -X POST http://127.0.0.1:8135/api/method/frappe.client.insert \
-H "Content-Type: application/json" \
-d '{"doc":{"doctype":"Sales Order","company":"Quzuo Trading","customer":"恒宇机械有限公司","transaction_date":"2026-10-27","delivery_date":"2026-11-10","selling_price_list":"Standard Selling","currency":"CNY","items":[{"item_code":"WM-1000","qty":5,"rate":3200.00}]}}'
实测回显(节选):
{"name": "SAL-ORD-2026-00001", "customer": "恒宇机械有限公司", "status": "Draft", "grand_total": 16000.0}
提交订单(走 Desk 表单的 savedocs 接口,与浏览器「提交」按钮同路径;--data-urlencode 提交完整文档 JSON):
# 先取完整文档
curl -s -b /tmp/erpcookie.txt -X POST http://127.0.0.1:8135/api/method/frappe.client.get \
-H "Content-Type: application/json" \
-d '{"doctype":"Sales Order","name":"SAL-ORD-2026-00001"}' -o /tmp/so.json
# 以 Submit 动作保存(doc=完整文档 JSON,action=Submit)
curl -s -b /tmp/erpcookie.txt -X POST http://127.0.0.1:8135/api/method/frappe.desk.form.save.savedocs \
--data-urlencode "doc@/tmp/so.json" --data-urlencode "action=Submit"
实测回显(节选,返回已提交文档):
{"docs":[{"name":"SAL-ORD-2026-00001",...,"docstatus":1,...}]}
最终状态确认:
curl -s -b /tmp/erpcookie.txt -X POST http://127.0.0.1:8135/api/method/frappe.client.get_value \
-H "Content-Type: application/json" \
-d '{"doctype":"Sales Order","name":"SAL-ORD-2026-00001","fieldname":["status","docstatus","grand_total","per_billed"]}'
{"message":{"status":"To Deliver and Bill","docstatus":1,"grand_total":16000.0,"per_billed":0.0}}
库存联动确认(Bin 余额表):
curl -s -b /tmp/erpcookie.txt -X POST http://127.0.0.1:8135/api/method/frappe.client.get_list \
-H "Content-Type: application/json" \
-d '{"doctype":"Bin","fields":["item_code","warehouse","actual_qty","stock_value"],"limit_page_length":5}'
{"message":[{"item_code":"WM-1000","warehouse":"Stores - QT","actual_qty":50.0,"stock_value":105000.0}]}
至此链路闭环:客户与物料主数据 → 销售订单 5×3200=16000 元 → 提交后状态进入 To Deliver and Bill(待发货待开票)→ 仓库 Stores - QT 账面 50 台、库存价值 105000 元(50×2100 估值入账)。
4.8 服务逐一起停演练
日常运维必须清楚「停某个服务时用户侧表现是什么」。实测两组场景:
场景 A:停入口 frontend(全站不可达):
cd /opt/erpnext && docker compose -f pwd.yml stop frontend
Container erpnext-v16-stack-frontend-1 Stopping
Container erpnext-v16-stack-frontend-1 Stopped
curl -s -m 3 -o /dev/null -w "HTTP %{http_code}\n" http://127.0.0.1:8135/
HTTP 000
curl 连接失败(exit 7)
exit 7 为「连接被拒绝」——入口进程已停,宿主端口无监听。重新拉起(实测回显):
docker compose -f pwd.yml start frontend
Container erpnext-v16-stack-configurator-1 Starting
Container erpnext-v16-stack-configurator-1 Started
Container erpnext-v16-stack-configurator-1 Waiting
Container erpnext-v16-stack-configurator-1 Exited
Container erpnext-v16-stack-frontend-1 Starting
Container erpnext-v16-stack-frontend-1 Started
sleep 5 && curl -s -m 5 -o /dev/null -w "恢复后 HTTP %{http_code}\n" http://127.0.0.1:8135/
恢复后 HTTP 200
💡 说明:start frontend 时 configurator 被重新执行了一次(
Exited为预期行为)——因为 frontend 通过 depends_on 条件绑定了 configurator 的完成状态,Compose 会先重跑该一次性任务再启动入口。configurator 幂等重写配置,重复执行无副作用。
场景 B:停应用 backend(入口在线但业务请求失败):
docker compose -f pwd.yml stop backend
curl -s -m 5 -o /dev/null -w "ping HTTP %{http_code}\n" http://127.0.0.1:8135/api/method/ping
ping HTTP 000
docker compose -f pwd.yml start backend && sleep 8 \
&& curl -s -m 5 http://127.0.0.1:8135/api/method/ping
{"message":"pong"}
对比结论:frontend 停 = 连接拒绝(端口无人监听);backend 停 = 入口进程还在但上游不可用(连接被关闭,HTTP 000)。生产排障时先看这一层差异即可定位是入口层还是应用层故障。
全栈空闲态资源占用实测(docker stats --no-stream,2026-10-27):
NAME MEM USAGE / LIMIT CPU %
frontend 6.652MiB / 19.48GiB 0.00%
backend 223.2MiB / 19.48GiB 0.01%
scheduler 55.92MiB / 19.48GiB 0.00%
queue-long 48.94MiB / 19.48GiB 0.00%
queue-short 52.48MiB / 19.48GiB 0.00%
websocket 59.61MiB / 19.48GiB 0.00%
redis-cache 9.133MiB / 19.48GiB 0.08%
redis-queue 10.29MiB / 19.48GiB 0.07%
db 172.8MiB / 19.48GiB 0.03%
合计约 639 MiB(空闲态)。内存大头是 backend(Gunicorn 多进程)与 db(MariaDB 缓冲池),扩容评估以这两项为准。
4.9 首次备份验证
部署收尾前做一次备份,确认备份通道可用(备份体系详见五、5.3 与《ERPNext v16 常用命令与运维实战》):
cd /home/frappe/frappe-bench && bench --site frontend backup --with-files
Backup Summary for frontend at 2026-10-28 00:48:54.462911
Config : /home/frappe/frappe-bench/sites/frontend/private/backups/20261028_004852-frontend-site_config_backup.json 176.0B
Database: /home/frappe/frappe-bench/sites/frontend/private/backups/20261028_004852-frontend-database.sql.gz 999.1KiB
Public : /home/frappe/frappe-bench/sites/frontend/private/backups/20261028_004852-frontend-files.tar 10.0KiB
Private : /home/frappe/frappe-bench/sites/frontend/private/backups/20261028_004852-frontend-private-files.tar 10.0KiB
Backup for Site frontend has been successfully completed with files
四件套齐备:站点配置、数据库压缩 SQL、公共文件与私有文件 tar 包。
五、数据持久化说明
栈共声明 4 个数据卷,全部数据落在这 4 个卷内;卷在宿主的真实位置为 /var/lib/docker/volumes/erpnext-v16-stack_<卷名>/_data:
| 卷名 | 挂载点(应用侧) | 内容 | 重要性 |
|---|---|---|---|
| sites | /home/frappe/frappe-bench/sites | 全部站点目录(site_config.json、备份、上传文件)、common_site_config.json、apps.txt | 最高——含站点配置与业务附件 |
| db-data | /var/lib/mysql | MariaDB 全部业务数据库 | 最高——业务数据本体 |
| logs | /home/frappe/frappe-bench/logs | 各进程日志 | 中 |
| redis-queue-data | /data | 队列持久化(未消费任务) | 中——丢队列即丢未执行的异步任务 |
要点:
- sites 与 db-data 是备份重点。前者存配置与文件备份,后者是数据本体;redis-cache 未挂卷(缓存丢了可重建),属官方有意设计;
- 备份产物落在
sites/frontend/private/backups/,与站点同卷——生产上应把备份再外送到远端(对象存储/NFS),避免与业务数据同生共死; - 升级镜像版本前先
bench backup --with-files,升级失败可用备份回滚。
六、环境变量表
编排中各服务的环境变量逐项说明(按服务分组):
backend / db(数据库连接与凭据):
| 变量 | 实测值 | 说明 |
|---|---|---|
| DB_HOST | db | 数据库主机名(编排内服务名) |
| DB_PORT | 3306 | 数据库端口 |
| MYSQL_ROOT_PASSWORD | q1w2e3r4 | 数据库 root 密码(建库建站时用) |
| MARIADB_ROOT_PASSWORD | q1w2e3r4 | MariaDB 镜像同义变量,两处保持一致 |
configurator(配置初始化):
| 变量 | 实测值 | 说明 |
|---|---|---|
| DB_HOST / DB_PORT | db / 3306 | 写入 common_site_config.json 的 db_host/db_port |
| REDIS_CACHE | redis-cache:6379 | 写入 redis_cache 配置项 |
| REDIS_QUEUE | redis-queue:6379 | 写入 redis_queue 与 redis_socketio 配置项 |
| SOCKETIO_PORT | 9000 | 写入 socketio_port 配置项 |
frontend(入口行为):
| 变量 | 实测值 | 说明 |
|---|---|---|
| BACKEND | backend:8000 | 动态请求上游(Gunicorn) |
| SOCKETIO | websocket:9000 | 实时推送上游 |
| FRAPPE_SITE_NAME_HEADER | frontend | 默认站点名:请求未带站点标识时按此路由(单站点部署即写站点名) |
| UPSTREAM_REAL_IP_ADDRESS | 127.0.0.1 | 真实客户端 IP 解析来源(前置反代时改为其地址) |
| UPSTREAM_REAL_IP_HEADER | X-Forwarded-For | 从该头取真实 IP |
| UPSTREAM_REAL_IP_RECURSIVE | off | 多级代理时改 on 逐级回溯 |
| PROXY_READ_TIMEOUT | 120 | 上游读超时(秒);跑长报表/导入加大此值 |
| CLIENT_MAX_BODY_SIZE | 50m | 上传体积上限;大附件场景调大 |
worker / websocket(队列与推送):
| 变量 | 实测值 | 说明 |
|---|---|---|
| FRAPPE_REDIS_CACHE | redis://redis-cache:6379 | Worker/推送进程的缓存连接 |
| FRAPPE_REDIS_QUEUE | redis://redis-queue:6379 | Worker/推送进程的队列连接 |
七、配置参数说明
栈初始化落盘两份核心配置文件,均在 sites 卷内(应用侧路径 /home/frappe/frappe-bench/sites/),参数含义的完整展开见《ERPNext v16 配置参数详解》。
全栈共享配置 common_site_config.json(configurator 自动生成,实测内容):
{
"db_host": "db",
"db_port": 3306,
"default_site": "frontend",
"redis_cache": "redis://redis-cache:6379",
"redis_queue": "redis://redis-queue:6379",
"redis_socketio": "redis://redis-queue:6379",
"socketio_port": 9000
}
逐项解读:db_host/db_port 指向数据库服务;default_site 决定未带 Host 标识请求的默认站点;redis_* 三项分工——cache 专缓存、queue 专任务队列、socketio 复用 queue 实例(官方单栈的省实例设计);socketio_port 与 websocket 进程监听端口对应。
站点级配置 frontend/site_config.json(建站自动生成,实测内容):
{
"db_name": "_b014443fe3d53c1a",
"db_password": "ujHrvgH2Q6Axb6R2",
"db_type": "mariadb",
"db_user": "_b014443fe3d53c1a",
"installed_apps": [
"frappe",
"erpnext"
]
}
逐项解读:db_name/db_user 为建站时自动创建的数据库与同名用户(随机下划线前缀命名);db_password 为随机生成的库用户密码(泄露此文件等于泄露站点数据,权限收紧);installed_apps 记录已装应用清单,站点迁移与升级校验都依赖它。
⚠️ 注意:改这两份配置的正规姿势是
bench set-config命令而非手工编辑(避免 JSON 手误与进程缓存不一致),命令用法见《ERPNext v16 常用命令与运维实战》。
八、配置文件挂载
两份配置文件都在 sites 卷内,持久化由卷承载,无需额外 bind mount。常见挂载需求与做法:
① 把站点文件放宿主目录管理(改用 bind mount,便于直接查看与外送备份):
# 编排中把 sites 卷换成 bind mount(示意,backend/create-site/frontend 等 6 处同步替换)
volumes:
- /opt/erpnext/sites:/home/frappe/frappe-bench/sites
- logs:/home/frappe/frappe-bench/logs
切换前必须先把原卷内容拷到宿主目录(卷内已有站点数据,直接换空目录会「丢站」):
cp -a /var/lib/docker/volumes/erpnext-v16-stack_sites/_data/. /opt/erpnext/sites/
② 备份目录单独外送:定期从 /var/lib/docker/volumes/erpnext-v16-stack_sites/_data/frontend/private/backups/ 同步到远端存储,或按官方生产路径给 worker 挂 S3 凭据用 bench backup --with-files 配合 push 备份。
③ MariaDB 数据落宿主:把 db-data 卷换成 /opt/erpnext/db:/var/lib/mysql bind mount,数据库文件直接可见;注意目录属主需为 MariaDB 镜像内 mysql 用户(uid 999),权限不对会导致起库失败。
九、各服务进程命令对照
多服务栈没有「一条命令等价跑起全栈」的形态(9 个常驻进程 + 2 个一次性任务各司其职)。为便于理解编排本质,列出各服务的进程命令对照(与编排 command 字段一致):
| 服务 | 进程命令 | 角色 |
|---|---|---|
| backend | bench start(内含 gunicorn 绑 8000 端口) | Web 应用(WSGI) |
| frontend | nginx-entrypoint.sh | Nginx 入口,监听 8080 |
| websocket | node /home/frappe/frappe-bench/apps/frappe/socketio.js | Socket.IO 推送 |
| queue-long | bench worker --queue long,default,short | 重任务 Worker |
| queue-short | bench worker --queue short,default | 轻任务 Worker |
| scheduler | bench schedule | 定时调度 |
| configurator | bench set-config -g …(6 条,跑完退出) | 配置初始化 |
| create-site | bench new-site --install-app erpnext --set-default frontend(跑完退出) | 建站 |
| db | mariadb --character-set-server=utf8mb4 … | 数据库 |
解读:全部应用进程共用同一镜像与同一 bench 工程(/home/frappe/frappe-bench),差异只在启动命令——这正是 Frappe「多站点多角色共用一份代码」的架构在编排层的体现。
十、常用运维命令
日常运维的高频命令速查(均在编排文件所在目录执行):
cd /opt/erpnext
# 查看全栈状态(含一次性任务的退出码)
docker compose -f pwd.yml ps
# 看某服务日志(最近 200 行并跟随)
docker compose -f pwd.yml logs -f --tail 200 backend
# 停/起单个服务
docker compose -f pwd.yml stop frontend
docker compose -f pwd.yml start frontend
# 重启单个服务(restart = stop+start 一条命令)
docker compose -f pwd.yml restart backend
# 全栈停止(保留数据卷)与全栈启动
docker compose -f pwd.yml stop
docker compose -f pwd.yml up -d
# 全栈彻底删除(含数据卷,谨慎:业务数据一并清除)
docker compose -f pwd.yml down -v
docker compose -f pwd.yml ps 实测回显(节选,注意两个 Exited (0) 是预期的一次性任务):
erpnext-v16-stack-configurator-1 Exited (0)
erpnext-v16-stack-create-site-1 Exited (0)
erpnext-v16-stack-db-1 Up 5 hours (healthy)
erpnext-v16-stack-frontend-1 Up 5 hours
bench 侧的备份/恢复/迁移/控制台等命令体系(bench backup/restore/migrate/new-site/console/execute)与排障实战见《ERPNext v16 常用命令与运维实战》;站点配置参数的完整语义见《ERPNext v16 配置参数详解》。
十一、K8s 部署形态(官方 Helm Chart 溯源)
本章为联网溯源内容(来源标注于各小节,查询日期 2026-10-27),k8s 集群实测将在后续 K8s 实测批次完成,本节不虚构集群实测回显。
ERPNext 官方提供 Kubernetes Helm Chart(仓库 github.com/frappe/helm,Chart 站点 helm.erpnext.com),是容器平台上的标准化交付路径。
Chart 版本与匹配关系(helm.erpnext.com 首页实查,2026-10-27):
| Chart 版本 | App Version | 发布日期 |
|---|---|---|
| erpnext-8.0.89 | v16.52.1 | 2026-10-22 |
| erpnext-8.0.88 | v16.52.0 | 2026-10-21 |
| erpnext-8.0.87 | v16.51.2 | 2026-10-21 |
解读:Chart 8.0.x 线与 ERPNext v16 逐版跟进,最新 chart 8.0.89 对应 v16.52.1,与本文 Compose 实测版本一致。
部署资源全景(官方 chart README 实查,2026-10-27):
- Deployment 六件套:gunicorn(Web)、nginx(入口+静态资源)、scheduler(调度)、socketio(推送)、worker-d/worker-l/worker-s(default/long/short 三级队列 Worker,共七个部署角色);
- HPA 六件套:gunicorn、nginx、socketio 与三级 worker 均配 HorizontalPodAutoscaler,可按负载横向扩缩;
- Job 全家桶:bench-conf(写配置)、create-site / create-multiple-sites(建站)、drop-site(删站)、backup-push(备份并可推 S3)、migrate(升级迁移)、clear-cache(升级后清缓存)、vol-fix(修卷权限)、custom(自定义命令)——升级、备份、建站全部 Job 化,可重复执行;
- 存储:sites 与 logs 两个 PVC;官方 README 明确要求站点卷使用 ReadWriteMany(RWX)访问模式的存储类——因为多个 Deployment 需同时挂载同一份站点文件(Compose 形态是单点共享卷,k8s 上拆成多副本后必须共享文件系统);
- 凭据:Secret 存 db-root-password(支持外接数据库)。
最小安装命令(官方 README 原样,实际集群验证待 K8s 实测批次):
kubectl create namespace erpnext
helm repo add frappe https://helm.erpnext.com
helm repo update
helm upgrade --install frappe-bench --namespace erpnext frappe/erpnext \
--set persistence.worker.storageClass=nfs
形态对比小结:Compose 形态适合单机自托管与试用(本文主线),Helm 形态适合容器平台标准化交付(HPA 弹性 + Job 化运维 + 多副本),但引入了 RWX 共享存储这一额外前置条件;两条路径的站点数据结构一致(都是 bench sites 目录),迁移时可互备互恢复。
十二、常见问题(FAQ)
Q1:create-site 失败,报 "could not find sites/common_site_config.json with required keys"?
create-site 有 120 秒超时轮询:等 configurator 把 db_host/redis_cache/redis_queue 三个键写入 common_site_config.json。超时说明 configurator 没跑成。定位:
docker compose -f pwd.yml logs configurator
实测中 configurator 失败最常见原因是 db 未就绪(数据库初始化慢于 120 秒,低配磁盘偶发)。处理:修好 db 后单独重跑一次性任务再拉起全栈:
docker compose -f pwd.yml up -d --force-recreate create-site
create-site 幂等(站点目录存在即跳过建站),重复执行安全。
Q2:管理员密码是多少?忘记后怎么重置?
pwd.yml 建站命令以 --admin-password=q1w2e3r4 指定初始密码。忘记后用 bench 重置:
cd /home/frappe/frappe-bench && bench --site frontend set-admin-password 新密码
Q3:建客户报 "Cannot select a Group type Customer Group"?
客户分组是树形结构,Group 节点只是归类容器,不能直接挂客户。把 customer_group 换成叶子节点(Commercial/Government/Individual/Non Profit),查询可用分组的命令与回显见 4.7 排障实录。
Q4:REST API 调用报 "Error: No module named 'frappe.core.doctype.sales'"?
/api/resource/Sales Order 这类资源路径含空格,未 URL 编码时服务端把 Sales Order 错误解析成模块名 sales 而导入失败。两种解法:把空格编码为 Sales%20Order;或改用 POST /api/method/frappe.client.insert 传 {"doc":{...}}(本文 4.7 采用后者,不受路径转义影响)。
Q5:提交单据报 "Cannot edit standard fields" 或 TimestampMismatchError?
两条都是真实踩坑实录:
- 对 docstatus 直接
frappe.client.set_value会报Cannot edit standard fields——docstatus 是标准工作流字段,禁止直接改值; - 取文档后隔了一会儿再提交报
TimestampMismatchError: ... has been modified after you have opened it——文档被后台任务(调度器/Worker)更新过,乐观锁拦下旧版本。解法:重新取最新文档立即提交;或改用frappe.desk.form.save.savedocs(传完整文档 JSON +action=Submit,与浏览器提交按钮同路径),本文 4.7 即此方案。
Q6:宿主端口 8080 被占用怎么办?
编排 ports 字段改宿主侧端口即可(如本文的 "8135:8080")。确认端口占用情况用 ss -ltn | grep <端口>。
Q7:内存不足的表现与处理?
4 GiB 以下机器全栈启动后可能表现为主数据导入/报表时 OOM 或进程被杀。优先级:给 queue-long 降配或合并队列 → 减小 MariaDB 缓冲池 → 升配。实测本栈空闲态约 640 MiB(明细见 4.8),4 GiB 机器跑试用栈可行,正式使用建议 8 GiB。
十三、参考链接
- 官方部署仓库 frappe_docker(pwd.yml 与生产 compose 文件族):https://github.com/frappe/frappe_docker
- 官方 Helm Chart 站点:https://helm.erpnext.com
- Helm Chart 源码仓库:https://github.com/frappe/helm
- ERPNext 官网:https://erpnext.com
- ERPNext GitHub 仓库:https://github.com/frappe/erpnext
- 版本发布页:https://github.com/frappe/erpnext/releases
- 官方文档:https://docs.frappe.io/erpnext
- Docker Hub 镜像仓库:https://hub.docker.com/r/frappe/erpnext
- 本专栏背景认知:《ERPNext 介绍与选型指南》