ERPNext v16 Docker Compose 部署实战

基于官方 frappe_docker pwd.yml 多服务编排,从零部署 ERPNext v16.52.1:服务依赖与起停演练、站点初始化、管理员登录、客户/物料/销售订单全链路验证、数据持久化与备份实测,附官方 Helm Chart 的 K8s 部署形态溯源。

ERPNextERPNext项目管理2026/10/28

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 的默认选型,社区主流路径兼容性最稳,事务型补丁全按它回归
PostgreSQLFrappe 支持但非默认,部分应用/补丁偶有兼容边角已有 PG 运维体系时可评估,需充分测试

本方案采用官方默认 MariaDB 11.8,理由是与 pwd.yml、Helm Chart 两条官方路径保持一致,排障时可对齐社区绝大多数案例。

三、前置条件

部署前先确认三件事:软件就绪、端口空闲、资源够用。

  1. 安装 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
  1. 确认对外端口空闲(本方案用 8135,可按需换成其他空闲端口):
ss -ltn | grep 8135 || echo "8135 空闲"
8135 空闲
  1. 资源要求:官方多服务栈包含 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/mysqlMariaDB 全部业务数据库最高——业务数据本体
logs/home/frappe/frappe-bench/logs各进程日志中
redis-queue-data/data队列持久化(未消费任务)中——丢队列即丢未执行的异步任务

要点:

  1. sites 与 db-data 是备份重点。前者存配置与文件备份,后者是数据本体;redis-cache 未挂卷(缓存丢了可重建),属官方有意设计;
  2. 备份产物落在 sites/frontend/private/backups/,与站点同卷——生产上应把备份再外送到远端(对象存储/NFS),避免与业务数据同生共死;
  3. 升级镜像版本前先 bench backup --with-files,升级失败可用备份回滚。

六、环境变量表

编排中各服务的环境变量逐项说明(按服务分组):

backend / db(数据库连接与凭据):

变量实测值说明
DB_HOSTdb数据库主机名(编排内服务名)
DB_PORT3306数据库端口
MYSQL_ROOT_PASSWORDq1w2e3r4数据库 root 密码(建库建站时用)
MARIADB_ROOT_PASSWORDq1w2e3r4MariaDB 镜像同义变量,两处保持一致

configurator(配置初始化):

变量实测值说明
DB_HOST / DB_PORTdb / 3306写入 common_site_config.json 的 db_host/db_port
REDIS_CACHEredis-cache:6379写入 redis_cache 配置项
REDIS_QUEUEredis-queue:6379写入 redis_queue 与 redis_socketio 配置项
SOCKETIO_PORT9000写入 socketio_port 配置项

frontend(入口行为):

变量实测值说明
BACKENDbackend:8000动态请求上游(Gunicorn)
SOCKETIOwebsocket:9000实时推送上游
FRAPPE_SITE_NAME_HEADERfrontend默认站点名:请求未带站点标识时按此路由(单站点部署即写站点名)
UPSTREAM_REAL_IP_ADDRESS127.0.0.1真实客户端 IP 解析来源(前置反代时改为其地址)
UPSTREAM_REAL_IP_HEADERX-Forwarded-For从该头取真实 IP
UPSTREAM_REAL_IP_RECURSIVEoff多级代理时改 on 逐级回溯
PROXY_READ_TIMEOUT120上游读超时(秒);跑长报表/导入加大此值
CLIENT_MAX_BODY_SIZE50m上传体积上限;大附件场景调大

worker / websocket(队列与推送):

变量实测值说明
FRAPPE_REDIS_CACHEredis://redis-cache:6379Worker/推送进程的缓存连接
FRAPPE_REDIS_QUEUEredis://redis-queue:6379Worker/推送进程的队列连接

七、配置参数说明

栈初始化落盘两份核心配置文件,均在 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 字段一致):

服务进程命令角色
backendbench start(内含 gunicorn 绑 8000 端口)Web 应用(WSGI)
frontendnginx-entrypoint.shNginx 入口,监听 8080
websocketnode /home/frappe/frappe-bench/apps/frappe/socketio.jsSocket.IO 推送
queue-longbench worker --queue long,default,short重任务 Worker
queue-shortbench worker --queue short,default轻任务 Worker
schedulerbench schedule定时调度
configuratorbench set-config -g …(6 条,跑完退出)配置初始化
create-sitebench new-site --install-app erpnext --set-default frontend(跑完退出)建站
dbmariadb --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.89v16.52.12026-10-22
erpnext-8.0.88v16.52.02026-10-21
erpnext-8.0.87v16.51.22026-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。

十三、参考链接

  1. 官方部署仓库 frappe_docker(pwd.yml 与生产 compose 文件族):https://github.com/frappe/frappe_docker
  2. 官方 Helm Chart 站点:https://helm.erpnext.com
  3. Helm Chart 源码仓库:https://github.com/frappe/helm
  4. ERPNext 官网:https://erpnext.com
  5. ERPNext GitHub 仓库:https://github.com/frappe/erpnext
  6. 版本发布页:https://github.com/frappe/erpnext/releases
  7. 官方文档:https://docs.frappe.io/erpnext
  8. Docker Hub 镜像仓库:https://hub.docker.com/r/frappe/erpnext
  9. 本专栏背景认知:《ERPNext 介绍与选型指南》