ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

ERPNext 14部署避坑指南:Docker与docker-compose常见故障排查

ERPNext 14部署避坑指南:Docker与docker-compose常见故障排查

简介:面向需要在虚拟机中快速部署ERP系统的开发者和运维人员,这份配套文档以Ubuntu 22.04虚拟机为基础,完整记录了使用Docker安装ERPNext 14的流程。内容聚焦实际部署,先从更新系统软件包、安装Git开始,逐步讲解Docker及Docker Compose的安装方法,包括添加阿里云稳定版源、配置GPG密钥、创建docker用户组等细节;还专门针对Docker Hub拉取失败的问题,给出了多个国内镜像加速器的自行配置方案。随后通过下载开箱即用版仓库并执行docker compose命令,即可在Linux浏览器中打开本地地址进入系统。包体为单个docx文档,仅321KB,步骤清晰且命令完整,适合有一定Linux基础、希望避开复杂配置快速获得ERPNext核心功能的读者。已有629人学习下载,对于需要借助容器技术验证ERP业务场景的团队来说,这是一份便捷的参考。

1. 部署 ERPNext 14 前,先搞清 docker-compose 为什么最容易在这三处翻车

docker compose up -d 跑完、镜像全部拉取成功,浏览器打开 http://localhost/80 却只看到连接被拒绝——这是我在虚拟机上部署 ERPNext 14 时见过最多的翻车现场,十次里有六次栽在这一步。原因可能出在 URL 端口写法、80 端口被占用、数据库容器还在初始化,甚至只是 docker 服务没设开机自启。Docker 部署 ERPNext 14 号称开箱即用,但容器化把环境差异藏进了黑匣子,报错反而更难定位。本笔记按「装 Docker → 配加速 → 起容器 → 打开页面」这条线,把每一步的典型报错、根因和验证命令拆开讲,新手能照着命令解决,熟手也能快速定位根因。

2. Ubuntu 22.04 装 Docker:GPG 密钥、用户组、compose 命令不存在的排查方法

2.1 apt 源和 GPG 密钥报错的根因与处理

在 Ubuntu 22.04 里装 Docker,第一步不是 docker 本身,而是先把 apt 源和 GPG 密钥处理对。很多人卡在sudo apt update那一步,终端里冒出一串NO_PUBKEY警告,后面跟一串十六进制字符,比如0EBFCD88。这其实是 apt 在告诉你:软件源的元数据没有通过签名校验,它不敢信任这个仓库。

0EBFCD88是 Docker 官方发布公钥的指纹,不是密码,也不是什么激活码。安装手册里通常会让你用 curl 把公钥拉下来,再通过gpg --dearmor转成 apt 能读的二进制格式,放到/etc/apt/trusted.gpg.d/目录下。注意要先执行sudo -i进入 root 环境,否则 curl 写文件时会遇到权限拒绝,这正是文档里为什么要先切 root 再做这一串操作的原因。

sudo -i curl -fsSL https://mirrors.aliyun.com/docker-ce/linux/ubuntu/gpg | gpg --dearmor -o /etc/apt/trusted.gpg.d/docker-ce.gpg apt-key fingerprint 0EBFCD880EBFCD88

curl -fsSL里的-f表示失败就退出,-sS是静默但保留错误输出,-L跟随重定向,缺一不可。gpg --dearmor把 ASCII 格式的公钥块转成二进制,apt 才能识别。这条命令执行完基本不会有什么输出,正常现象。接着用apt-key fingerprint验证,能看到公钥信息说明导入成功,看不到就回到上一步重新检查网络。

再往后添加软件源那一步,命令里用了$(lsb_release -cs)。这个命令会输出当前系统的发行版代号,Ubuntu 22.04 输出的是jammy。如果是在别的版本上操作,得到的代号不同,apt 源路径就完全不一样。这里有个我踩过的坑:如果系统里没装lsb-release,这个变量是空的,源会拼接成一个 404 地址,apt update就报错。解决方法是先执行sudo apt install lsb-release,再重新拼接。

2.2 usermod 加了 docker 用户组却还报 permission denied

装完 docker-ce 之后,文档里会让你执行sudo usermod -aG docker 你的用户名,目的是让当前用户免 sudo 直接操作 docker。很多人执行完这条命令,马上运行docker images,结果直接报permission denied while trying to connect to the Docker daemon socket。

这不是 docker 没装好,而是用户组变更没有生效。Linux 的用户组信息在登录时加载到会话里,你给当前用户追加了一个组,当前这个 shell 会话却还拿着旧的身份凭据。常见做法有两种:一是执行su - 你的用户名重新登录一次,相当于刷新整个会话;二是干脆注销虚拟机重新登录。我个人更推荐su -,速度快,不影响其他终端。

sudo usermod -aG docker 你的用户名 su - 你的用户名 docker images

如果重新登录之后还是报同样的 permission denied,那就不是用户组的问题了,需要检查 docker socket 的属主。ls -l /var/run/docker.sock,正常情况下应该显示srw-rw---- root docker,也就是说 root 和 docker 组拥有读写权限。如果 socket 文件的属组不是 docker,而是别的组,那就手动改一下:sudo chgrp docker /var/run/docker.sock。不过这种情况很少见,更常见的是用户根本没有重新登录,所以看到报错先别急着改权限,重新登录一次再说。

2.3 docker-compose: command not found——下载了却执行不了

Docker 装好之后还有一道坎:docker-compose。按照手册用 curl 下载到/usr/local/bin/docker-compose之后,执行docker-compose --version,却提示command not found。原因就两个:一是文件没有执行权限,curl 下载出来的文件默认是 644,只有读权限,没有执行权限;二是/usr/local/bin不在当前用户的 PATH 环境变量里,shell 找不到这个命令。

sudo curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose sudo chmod +x /usr/local/bin/docker-compose sudo ln -sf /usr/local/bin/docker-compose /usr/bin/docker-compose docker-compose --version

下载命令里的$(uname -s)和$(uname -m)会自动填充成当前系统的内核名和架构,比如 Linux x86_64,这样下载的就是匹配这个平台的二进制。chmod +x把执行权限补上。ln -sf是在/usr/bin下建一个软链接,如果当前用户的 PATH 里没有/usr/local/bin,还能通过/usr/bin找到它。最后docker-compose --version能输出版本号就说明装好了。

需要提醒的是,新版 Docker 已经内置了 Compose V2 插件,直接执行docker compose(中间是空格)就能用,和独立安装的docker-compose(带横线)是两套东西。在一个系统里两套并存容易造成混淆,比如某个脚本调用的命令和你手敲的命令版本不一致。我的建议是:教程里用哪个就固定用哪个。如果你跟着本文用独立二进制,后面所有 compose 命令就统一写docker-compose;如果你更习惯新写法,那就确认docker compose version能输出,然后统一用docker compose。

3. Docker 拉取镜像超时:配置镜像加速器后还是不生效的几种情况

3.1 error pulling image configuration:为什么改了 daemon.json 也没用

部署 ERPNext 14 时最折磨人的不是命令不对,而是镜像拉不下来。docker compose up -d执行后,进度条停在某个镜像上不动,过一会儿报error pulling image configuration: Get https://registry-1.docker.io/v2/...: net/http: request canceled,或者干脆timed out。这不是你命令敲错了,而是默认镜像源registry-1.docker.io所在的 Docker Hub 访问链路不稳定。虚拟机本身处于 NAT 双重网络环境下,这种问题会更明显。

解决办法是给 Docker 配置镜像加速器,也就是在/etc/docker/daemon.json里写入registry-mirrors字段。配置完成后要重启 Docker 才能生效,但很多人恰恰漏了这一步。改完 daemon.json 只执行了systemctl restart docker,没有执行systemctl daemon-reload,或者两个命令顺序反了。Docker 服务读取的是上一个版本的配置,加速器自然不生效。

sudo touch /etc/docker/daemon.json sudo vim /etc/docker/daemon.json

打开文件后写入以下内容。注意 JSON 格式不支持注释,也不要带多余逗号:

{ "registry-mirrors": [ "https://docker.m.daocloud.io", "https://mirror.aliyuncs.com", "https://docker.nju.edu.cn", "https://docker.mirrors.ustc.edu.cn" ] }

这里的地址只是示例,实际使用时可以换成你网络环境下能访问的公共镜像站。我一般会多填几条,Docker 会按顺序尝试,前面失败会自动切换下一条。写完保存,然后依次执行:

sudo systemctl daemon-reload sudo systemctl restart docker docker info | grep -A 8 "Registry Mirrors"

daemon-reload让 systemd 重新读取 service 文件,restart docker才真正让 dockerd 重新加载 daemon.json。最后一条docker info里的Registry Mirrors段落如果列出了你填写的地址,说明配置生效;没有列出,说明 daemon.json 根本没被读进去,回到文件路径和权限上查。

3.2 改了 daemon.json 不生效的三个原因

排错时先看docker info,这是判断加速器有没有生效的唯一标准。不生效常见有三种情况,按出现频率排:

第一种,改完没重启。配置文件改了,dockerd 还在内存里跑着旧配置,必须daemon-reload加restart docker两个命令都走完。只执行其中一个,或者只执行了restart docker没执行 reload,都会出现「改了等于没改」的假象。

第二种,JSON 写错了。daemon.json对格式极其敏感,多了一个逗号、用了单引号、或者写了注释,dockerd 启动时解析失败,直接拒绝启动。这时systemctl status docker会显示 failed,journalctl -u docker里能看到 JSON 解析报错。这种情况其实比不生效更严重,因为 Docker 整个服务都起不来了。所以写完配置文件先校验一下格式:

python3 -m json.tool /etc/docker/daemon.json

这个命令会把 JSON 格式化输出,有语法错误会明确告诉你第几行有问题。没有 python3 的话,先sudo apt install python3。

第三种,文件路径或权限有问题。配置文件必须放在/etc/docker/daemon.json,不能是/etc/daemon.json,也不能是子目录。另外权限方面有个大坑:手册里让你chmod 777 -R /etc/docker/daemon.json,这个操作在新版 Docker 上会直接导致启动失败,报open /etc/docker/daemon.json: permission denied。Docker 认为全局可写的配置文件是不可信的,属于安全问题。正规做法是把文件权限改成 644:

sudo chmod 644 /etc/docker/daemon.json

「配置 777 权限」属于典型的把 Windows 思维带进 Linux 的操作,不但多余,还会引入新的故障。

3.3 加速器列表怎么选,以及不配置的风险

镜像加速器的本质是 Docker Hub 的只读缓存节点。公共镜像站按来源大致分三类:云厂商提供的公共镜像站、高校网络维护的站点、社区自建站点。它们的可用性受网络环境影响很大,今天能用的地址,过两周可能就拉不动了,这不是配置问题,而是运维策略问题。所以我的习惯是在 daemon.json 里保留至少三到四条可用的地址,并且在笔记里记录一下填写日期。一旦发现某天拉镜像又开始超时,先用curl -I 镜像站地址测一下能不能通,把失效的地址替换掉就行。

不配置加速器能不能装 ERPNext 14?能,但要在网络通畅的时间反复重试,一次pull可能需要重试十几次才能把一个镜像拉完整,而 ERPNext 的 compose 文件涉及 MariaDB、Redis、Nginx、frappe 前端后端等多个镜像,总数据量好几个 GB,重试成本太高。我在虚拟机里第一次装的时候没配加速器,光是拉镜像就耗了将近两小时,后来配上加速器,同样的镜像几分钟就拉完了。

另外注意一个细节:daemon.json里的registry-mirrors只影响镜像拉取阶段,容器运行时的网络不受它的管控。换句话说,假如 ERPNext 容器启动后访问外网失败,问题不在加速器配置,而在宿主机网络或容器网络本身,别把两个问题混在一起查。

4. compose 部署 ERPNext 14:容器起不来、初始化卡死、浏览器打不开

4.1 80 端口被占用的处理与端口映射调整

镜像拉完,执行docker compose --project-name erpnext_oob -f pwd.yml up -d,这行命令本身没错,但它背后隐藏着一个高频故障:宿主机 80 端口被占。ERPNext 的 nginx 容器默认要映射宿主机的 80 端口,如果虚拟机上已经装了 nginx、apache,或者跑了其他 Web 服务,容器启动时会报bind: address already in use,然后容器直接退出。

判断端口占用用两条命令,任选其一:

sudo ss -tlnp | grep :80 sudo lsof -i:80

ss -tlnp里的-t看 TCP,-l只显示监听状态,-n不解析域名,-p显示进程名。看到输出里有进程占着 0.0.0.0:80,就是它抢了 ERPNext 的端口。处理方式是停掉占用进程,或者改 ERPNext 的映射端口。我一般倾向于改映射端口,保留宿主机上原有的服务不动。编辑pwd.yml,找到 nginx 服务下的ports段,把"80:80"改成"8080:80",左边是宿主机端口,右边是容器端口,改完后重新创建容器:

docker compose --project-name erpnext_oob -f pwd.yml down docker compose --project-name erpnext_oob -f pwd.yml up -d

注意先down再up,直接up不会重新读取端口映射。改完后 ERPNext 的访问地址就变成了http://虚拟机IP:8080。这行命令里的--project-name erpnext_oob是项目名,作用是把这一组容器归到一个独立命名空间,避免多个 compose 项目之间的资源名冲突。如果你只部署了这一个项目,不指定也行,但指定了之后所有的docker compose ps、docker compose logs都要带着-p erpnext_oob,否则只能看到一个空列表。

另外还有一个容器反复重启的经典场景:frappe 容器启动时数据库还没就绪。compose 里的 MariaDB 容器正在初始化,ERPNEXT 容器急着去连数据库,连接失败就会重启,docker compose ps 里能看到Restarting状态。这不一定代表故障,首次部署时数据库容器要建库、执行初始化脚本,frappe 容器需要不断重试直到数据库能连上。观察五分钟,如果容器从 Restarting 变成 Up,说明恢复了。

4.2 浏览器访问 localhost/80 打不开:URL 写法与虚拟机网络模式

ERPNext 部署完,手册最后告诉你「在 Linux 的浏览器火狐中打开链接:http://localhost/80」。如果你真的照着敲了这个地址,火狐会给你一个 404 或者「无法访问此网站」。问题出在 URL 语法上:/80被浏览器解析成了路径,而不是端口。正确的写法是http://localhost:80/,因为 80 是 HTTP 默认端口,直接写http://localhost/也能访问。

但这个问题还藏着一层:如果你是在 Windows 宿主机上打开浏览器,访问http://localhost指向的是 Windows 本机,不是虚拟机里的 Ubuntu。VMware 虚拟机虽然和宿主机共享网络,但 localhost 永远只表示当前操作系统的回环地址。宿主机想访问虚拟机里的 ERPNext,必须用虚拟机的 IP 地址。

获取虚拟机 IP 的方式:

ip -4 addr show | grep inet

看到形如192.168.x.x的地址就是虚拟机的 IP。在宿主机浏览器输入http://192.168.x.x访问。VMware 网络模式这里值得多说一句。NAT 模式下虚拟机会从 VMware 的虚拟网卡获取一个独立 IP,宿主机可以通过这个 IP 访问虚拟机,这是最简单的配置;桥接模式下虚拟机和宿主机在同一个局域网段,也能互通。不管哪种模式,只要 IP 能 ping 通、Ubuntu 防火墙没有挡 80 端口(执行过sudo ufw status确认,如果开着就sudo ufw allow 80/tcp),页面就能打开。别一上来怀疑 ERPNext 部署失败,先在虚拟机里自测:

curl -I http://localhost

看到HTTP/1.1 302或200,说明 nginx 已经正常响应,剩下的就是宿主机访问路径的问题了。

4.3 首次启动十分钟内页面没反应:数据库初始化与日志观察

docker compose up -d返回成功不代表 ERPNext 就能用了。第一次启动时,compose 会创建 MariaDB 数据库、初始化 ERPNext 站点、执行数据库迁移脚本,这个过程在低配置的虚拟机上可能需要五到十五分钟。期间访问页面,要么白屏转圈,要么显示数据库正在初始化,都属于正常现象。我见过不少人以为部署失败,反复 down/up 重来,结果每次都被迫从头初始化,反而拖的更久。

正确做法是看容器日志判断真实进度:

cd erpnext_oob_docker docker compose --project-name erpnext_oob -f pwd.yml logs -f --tail=200

logs -f是持续输出日志,类似tail -f;--tail=200表示只显示最后 200 行,避免刷屏。日志里如果出现migrate、database setup、bench相关的字样,说明还在初始化阶段;出现nginx started、Listening on或者 frappe 的serve输出,说明服务已经就绪。判断容器状态的命令是docker compose ps,看到所有容器的 STATUS 都是Up,再配合 curl 测试,才算是真正部署完成。

初始化完成后的登录入口是/login,默认管理员账号是Administrator,密码是初始化过程中设置的密码。如果你用的是开箱即用版镜像,登录页面通常会有预设密码提示,直接按提示登录即可。登录进去之后,ERPNext 14 的财务、销售、采购、库存、项目管理这些模块都在侧边栏里,到这一步,整套部署才算真正跑通。

5. 避坑指南:我在 ERPNext 部署中踩过的 5 个典型坑

5.1 chmod 777 配置 daemon.json 导致 Docker 启动失败

现象:按手册执行chmod 777 -R /etc/docker/daemon.json后,systemctl restart docker报错,systemctl status docker显示 failed,日志里出现permission denied。

原因:新版 Docker 对配置文件的权限有校验,/etc/docker/daemon.json全局可写被认为是不安全状态,dockerd 直接拒绝加载。这个校验机制让「图省事给 777 权限」的习惯彻底翻车。

解决:把权限改回 644。sudo chmod 644 /etc/docker/daemon.json,然后systemctl restart docker。注意目录/etc/docker保持 755 即可,不需要修改。从那以后我配置这类文件一律用sudo vi编辑,保存时默认就保留了 root 属主和 644 权限,不再手动 chmod。

5.2 配置了镜像加速器但 docker info 里看不到

现象:daemon.json 里填好了 registry-mirrors,重启 docker 后,docker info的Registry Mirrors段落还是空的,pull 依然超时。

原因:要么没执行systemctl daemon-reload,dockerd 读的还是旧配置文件;要么 JSON 格式不规范,文件被忽略但 docker 没报错;要么配置文件权限 777 触发保护,内容未被加载。

解决:先python3 -m json.tool /etc/docker/daemon.json校验格式,再依次执行systemctl daemon-reload和systemctl restart docker,最后用docker info验证。我自己的排查顺序是:格式 → 权限 → reload → restart → info,一步都不能少。

5.3 docker compose 和 docker-compose 两个命令混用

现象:教程里用的是docker compose --project-name erpnext_oob -f pwd.yml up -d,有的同事在排查时用docker-compose ps,发现什么都查不到,误以为容器被删了。

原因:新版 docker 内置 Compose V2 插件,调用方式是docker compose(空格);独立安装的 Compose V1 二进制调用方式是docker-compose(横线)。两套版本号、子命令和配置文件兼容性存在差异,混用会造成误判。

解决:统一以你部署时使用的命令为准。部署用的docker compose,那么ps、logs、down就都写成docker compose ...。检查版本:docker compose version或docker-compose --version,知道系统里装了哪一套,再决定后续命令。别在一个排查流程里换着敲。

5.4 浏览器访问 localhost/80 永远打不开

现象:按照手册最后一句访问http://localhost/80,页面 404,或者显示无法连接。反复重新部署后还是同样结果。

原因:这个 URL 本身就是错的,/80被浏览器当作路径,访问的是 80 端口下不存在的资源;另外从宿主机浏览器访问时,localhost指向宿主机而不是虚拟机,指向对象也错了。

解决:虚拟机内先执行curl -I http://localhost确认服务正常,再执行ip -4 addr show获取虚拟机 IP,宿主机浏览器访问http://虚拟机IP。端口假如改成了 8080,就访问http://虚拟机IP:8080。这条坑让我意识到:手册里的 URL 也要带着怀疑去看,端口号这种细节最容易被写错。

5.5 VMware 重启后 ERPNext 打不开

现象:配置文件没问题、部署也成功,但虚拟机重启一次后,浏览器访问 ERPNext 超时。登录虚拟机一看,systemctl status docker显示 docker 服务根本没启动。

原因:docker 服务没有设置开机自启。安装完成后只执行过systemctl start docker,没执行systemctl enable docker,重启后 dockerd 不自动拉起,所有容器自然都离线。

解决:执行sudo systemctl enable docker,下次开机自动启动。另外提醒一点:如果 VMware 直接强行关机或者宿主机断电,虚拟机里的 docker 数据目录/var/lib/docker里的 overlay2 文件系统可能损坏,最典型的现象是docker images列表为空、容器状态异常。这种损坏没法靠命令修复,只能重建容器数据。所以虚拟机一定要正常关机,这比任何配置都重要。

6. 快速定位:用一套固定顺序确认 Docker 与 ERPNext 每个环节

6.1 从命令到容器再到页面:排查验证的四步顺序

遇到 ERPNext 访问不了,我一般不直接去点浏览器,而是从底层服务到上层应用按顺序过一遍。这套顺序适用于绝大多数容器化部署故障。

第一步确认 docker 服务本身。systemctl status docker --no-pager,看到active (running)再往下走。这一步经常能拦住一大半问题:服务没起,后面全是白查。

第二步确认镜像和容器资源。docker images看镜像是否齐全,docker compose --project-name erpnext_oob -f pwd.yml ps看容器状态。所有容器Up才算正常,有Restarting或Exited的,用docker compose --project-name erpnext_oob -f pwd.yml logs -f --tail=200看对应容器的报错。日志里bind: address already in use指向端口冲突,connection refused指向数据库未就绪,OOMKilled指向内存不足——这三种关键词基本覆盖了绝大部分启动失败原因。

第三步确认网络和端口。ss -tlnp | grep -E ':80|:8000',看看 80 端口是否在监听,监听的是哪个进程。容器内部端口映射是否正确,用docker port erpnext_oob-nginx-1之类的命令查看,注意容器名以实际docker compose ps输出为准。

第四步确认页面响应。curl -I http://localhost,返回 302 或 200 说明 nginx 正常;超时说明上层服务未就绪,回到第二步看日志。这四步每步都是独立验证点,通过一步再进入下一步,逻辑上没有死角。

6.2 常见报错速查表

现象常见原因排查命令
permission denied (daemon socket)用户未重新登录,docker 组不生效su - 用户名后重试
error pulling image configuration镜像加速未配置或失效docker info | grep -A 8 "Registry Mirrors"
bind: address already in use80 端口被宿主机其他进程占用ss -tlnp | grep :80
容器状态 Restarting数据库未就绪或内存不足docker compose ... ps+dmesg | tail
/etc/docker/daemon.json permission denied配置文件权限 777 触发保护chmod 644 /etc/docker/daemon.json
宿主机访问 localhost 失败指向了宿主机而非虚拟机ip -4 addr show获取虚拟机 IP

从那以后,我每次在 VMware 里部署这类容器化应用,都会强制走一遍:先看 docker 服务,再看镜像加速,然后容器状态和日志,最后才去碰浏览器。这套顺序帮我筛掉了一大半「以为是网络问题其实是端口问题」的假故障,也让我少了很多翻车时刻。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进