1. 问题现象与根因定位1.1 报错长什么样为什么让人一头雾水第一次碰到这个报错的人大概率会愣一下。Pipeline 脚本里明明写了docker build、docker push本地终端跑得好好的一放到 Jenkins 上就给你甩一句 docker build -t myapp:latest . /var/lib/jenkins/workspace/myapptmp/durable-xxxx/script.sh: line 3: docker: not found关键迷惑点在于Jenkins 所在的那台机器你 SSH 上去敲docker ps是正常的。于是很多人第一反应是Jenkins 坏了插件没装然后开始瞎折腾插件市场装了一堆 Docker Pipeline、Docker Commons、Docker API重启了八百遍问题依旧。真相往往很朴素Jenkins 执行 Pipeline 时的进程环境和你 SSH 登录时的 shell 环境根本不是同一个环境。你 SSH 登录走的是交互式登录 shell会加载/etc/profile、~/.bashrc、~/.bash_profile这一整套而 Jenkins agent 启动构建时走的是非交互式、非登录 shellPATH 往往只有系统默认的那几条/usr/local/bin、/usr/bin里没有 docker 这个可执行文件自然就not found。所以这个报错的本质不是docker 没装而是Jenkins 找不到 docker 这个命令。把这句话刻在脑子里后面所有排查都围绕它展开。1.2 三种典型场景对号入座根据我这些年踩坑的经验这个报错基本逃不出下面三种场景先判断自己属于哪一种能省掉大量无效排查。场景典型特征根因方向场景AJenkins 跑在宿主机docker 也装在宿主机SSH 能跑 dockerPipeline 报 not foundPATH 环境变量未继承场景BJenkins 跑在容器里docker 装在宿主机容器内根本没有 docker 二进制需要挂载 docker 或走 socket场景CJenkins 用 Kubernetes/远程 agent 动态起 Pod每次构建环境都是全新的agent 镜像里没装 docker CLI场景A最常见也最好解决核心就是 PATH。场景B和场景C稍微绕一点涉及容器与宿主机的边界问题后面会单独展开。1.3 先做一次体检别急着改配置动手之前先花两分钟确认现状避免改错方向。在 Jenkins 的 Pipeline 里临时插一段诊断脚本pipeline { agent any stages { stage(Diagnose) { steps { sh echo PATH$PATH sh which docker || echo docker not in PATH sh ls -l /usr/bin/docker /usr/local/bin/docker 2/dev/null || echo no docker binary in common paths sh id sh cat /etc/os-release | head -2 } } } }跑一次看输出。重点看三样东西PATH 里有没有 docker 所在目录、docker 二进制到底在哪个路径、当前执行用户是谁。这三条信息拿到手问题基本就定位了一半。我见过太多人跳过这一步直接改配置结果改了半天发现 docker 压根没装在那台 agent 上纯属白忙。提示诊断脚本用完记得删掉别留在生产 Pipeline 里输出环境信息虽然不敏感但也没必要长期暴露。2. 场景APATH 环境变量没继承最经典的坑2.1 为什么 SSH 能跑Jenkins 跑不了这里得把 shell 的加载机制讲清楚不然你永远在试而不是在改。Linux 下 shell 分两类登录 shelllogin shell和非登录 shellnon-login shell又分交互式interactive和非交互式non-interactive。SSH 登录进来的是交互式登录 shell它会依次读/etc/profile→~/.bash_profile→~/.bashrc而 docker 的安装脚本通常会把/usr/local/bin或者 docker 自己的 bin 目录写进/etc/profile.d/下的某个脚本里所以你 SSH 一登录PATH 就带上了。Jenkins 执行sh步骤时起的是一个非交互式非登录 shell它不读/etc/profile也不读~/.bash_profile最多读一下~/.bashrc还得看具体实现。于是/etc/profile.d/docker.sh里设置的 PATH 完全没生效docker 自然找不到。打个比方SSH 登录像是走正门进公司前台会给你一张门禁卡PATHJenkins 执行脚本像是从消防通道溜进去没人发卡你当然刷不开 docker 这扇门。2.2 三种修法从推荐到将就修法一在 Pipeline 里显式声明 PATH最推荐最干净、最可控的做法是在 Pipeline 的environment块里把 PATH 补全pipeline { agent any environment { PATH /usr/local/bin:/usr/bin:/bin:${env.PATH} } stages { stage(Build) { steps { sh docker build -t myapp:latest . } } } }这种写法的好处是作用域明确只影响这个 Pipeline不会污染全局也不会因为改了系统配置影响别的任务。缺点是每个 Pipeline 都得写一遍任务多了有点烦。解决办法是抽成共享库Shared Library或者用全局环境变量统一注入。修法二配置 Jenkins 全局环境变量在系统管理 → 系统配置 → 全局属性里勾选环境变量加一条PATH值填/usr/local/bin:/usr/bin:/bin。这样所有任务都能继承。但这里有个坑全局环境变量会覆盖而不是追加如果你直接填/usr/local/bin可能把系统原有的 PATH 冲掉导致sh、git这些命令也找不到。稳妥的写法是引用已有的PATH/usr/local/bin:/usr/bin:/bin:$PATH不过不同 Jenkins 版本对$PATH的解析行为不一致有的能展开有的不能实测下来最保险的还是修法一。修法三在 sh 步骤里临时 export最将就sh export PATH$PATH:/usr/local/bin docker build -t myapp:latest . 能用但每个sh块都得写而且一旦脚本里嵌套了别的 shell 调用export 不一定传得下去。只适合临时救急不建议长期用。2.3 一个容易被忽略的细节docker 到底装哪了很多人想当然地以为 docker 在/usr/bin/docker结果 PATH 加了/usr/bin还是找不到。实际上 docker 的安装方式不同二进制位置差别很大用系统包管理器装的apt/yum一般在/usr/bin/docker用官方脚本装的一般在/usr/bin/docker或/usr/local/bin/docker手动解压二进制包装的可能在你解压的任意目录比如/opt/docker/bin/docker所以先which docker或find / -name docker -type f 2/dev/null确认路径再往 PATH 里加别凭感觉。我见过有人 PATH 加了/usr/local/bin结果 docker 在/usr/bin白折腾一下午。实操心得确认路径后建议用ls -l看一眼是不是软链接。有些环境里/usr/bin/docker是个指向/usr/libexec/docker/docker的软链接PATH 加对了但软链接目标权限不对照样报错只是报错信息会变成 Permission denied 而不是 not found。3. 场景BJenkins 在容器里docker 在宿主机3.1 容器里的套娃困境现在越来越多团队把 Jenkins 本身也容器化了用官方jenkins/jenkins镜像跑。这时候问题就来了Jenkins 容器里没有 docker 命令因为官方镜像压根没装 docker CLI。有人第一反应是那我在容器里装一个 docker 不就行了。可以但装完你会发现新问题容器里的 docker 想连宿主机的 docker daemon得通过 socket 通信而 socket 默认没挂载进来于是报 Cannot connect to the Docker daemon。这就是典型的套娃困境你要在容器里用 docker但 docker 的 daemon 在宿主机上。解决思路有两条各有取舍。3.2 方案一挂载 docker socketDocker-out-of-Docker核心思路是把宿主机的/var/run/docker.sock挂进 Jenkins 容器再在容器里装个 docker CLI让它通过 socket 指挥宿主机的 daemon。启动 Jenkins 容器时这样写docker run -d \ --name jenkins \ -p 8080:8080 -p 50000:50000 \ -v jenkins_home:/var/jenkins_home \ -v /var/run/docker.sock:/var/run/docker.sock \ -v /usr/bin/docker:/usr/bin/docker \ jenkins/jenkins:lts这里挂了两个东西socket 和 docker 二进制。挂二进制是为了省去在容器里重新安装的麻烦直接把宿主机的 docker 命令映射进去。然后在 Pipeline 里因为 docker 已经在/usr/bin了PATH 默认就包含直接就能跑pipeline { agent any stages { stage(Build) { steps { sh docker version sh docker build -t myapp:latest . } } } }这个方案的好处是简单直接构建出来的镜像直接落在宿主机上后续部署方便。缺点是安全性差——挂载 socket 等于把宿主机的 docker 控制权交给了容器容器里能起特权容器能挂载宿主机任意目录隔离性基本为零。生产环境慎用内部测试环境无所谓。3.3 方案二用 Kaniko 或 Buildah 做无 daemon 构建如果对安全有要求或者跑在 K8s 里不方便挂 socket那就上无 daemon 构建工具。Kaniko 是 Google 开源的专门在容器里构建镜像不需要 docker daemon。Pipeline 里这样用pipeline { agent { kubernetes { yaml apiVersion: v1 kind: Pod spec: containers: - name: kaniko image: gcr.io/kaniko-project/executor:debug command: [sleep] args: [infinity] } } stages { stage(Build) { steps { container(kaniko) { sh /kaniko/executor \ --contextpwd \ --dockerfilepwd/Dockerfile \ --destinationmyregistry/myapp:latest } } } } }Kaniko 的原理是逐层解析 Dockerfile在用户空间直接操作文件系统并生成镜像层全程不碰 daemon。代价是构建速度比原生 docker 慢一些而且对某些 Dockerfile 语法支持不完全比如RUN --mount这类新特性早期版本不支持。Buildah 是另一条路更接近 docker 的使用习惯但配置稍复杂。选哪个看团队熟悉度我个人在 K8s 环境里更倾向 Kaniko因为它是为这个场景生的。3.4 两种方案怎么选维度挂载 socketKaniko/Buildah配置复杂度低中安全性差好构建速度快中等镜像缓存依赖 daemon需额外配置适用场景内网测试、单机K8s、多租户、生产一句话总结图省事挂 socket要安全上 Kaniko。别在测试环境纠结安全也别在生产环境图省事。4. 场景C动态 agent 镜像里没装 docker4.1 K8s agent 的每次都是新环境特性用 Kubernetes 插件动态起 Pod 做 agent 的团队这个报错出现频率极高。原因很简单每次构建都是全新的 Pod用的是你指定的镜像镜像里没装 docker那就没有。很多人被动态两个字迷惑以为 Jenkins 会自动带上环境其实不会。Pod 里有什么完全取决于你用的镜像。4.2 解决思路要么装要么换思路一用带 docker CLI 的镜像把 agent 镜像换成自带 docker CLI 的比如docker:latest或者自己基于jenkins/inbound-agent加装 dockerFROM jenkins/inbound-agent:latest USER root RUN apt-get update \ apt-get install -y docker.io \ rm -rf /var/lib/apt/lists/* USER jenkins构建推送到私有仓库然后在 Pipeline 里指定pipeline { agent { kubernetes { yaml apiVersion: v1 kind: Pod spec: containers: - name: jnlp image: myregistry/jenkins-agent-docker:latest } } stages { stage(Build) { steps { sh docker build -t myapp:latest . } } } }但注意光装 CLI 还不够还得让这个 Pod 能连上 docker daemon。要么挂宿主机的 socket通过 volume 挂载要么用 DinDDocker-in-Docker在 Pod 里再起一个 daemon。DinD 需要特权模式K8s 里默认不允许得改安全策略比较麻烦。思路二干脆不用 docker用 Kaniko跟场景B一样K8s 环境里最顺的还是 Kaniko。既然 Pod 是动态的那就让 Pod 里跑 Kaniko构建完把镜像推到仓库全程不依赖 daemon也不用特权模式。这是目前 K8s Jenkins 组合里最主流的做法。4.3 一个真实的排查案例之前帮一个团队排查过类似问题他们的 Pipeline 在 K8s agent 上跑报docker: not found。他们以为是 PATH 问题在 Pipeline 里加了一堆 PATH没用。后来一看 agent 镜像是个精简的alpine基础镜像里面连sh都是 busybox 的更别说 docker 了。换成带 docker CLI 的镜像后报错变成了 Cannot connect to the Docker daemon。再一看Pod 没挂 socket也没起 DinD。最后改成 Kaniko问题彻底解决。这个案例的教训是动态 agent 环境下PATH 往往不是根因镜像里有没有东西才是。排查顺序应该是先看镜像里有没有 docker再看能不能连 daemon最后才看 PATH。5. 常见问题速查与避坑清单5.1 问题速查表报错信息可能原因排查动作docker: not foundPATH 没继承 / 镜像没装诊断脚本看 PATH 和 whichCannot connect to the Docker daemonsocket 没挂 / daemon 没起检查 /var/run/docker.sockPermission denied用户不在 docker 组 / socket 权限id看用户组ls -l看 socketdocker: command not found注意空格拼写或别名问题检查脚本里的命令拼写构建成功但镜像找不到镜像构建在别的 daemon 上确认 daemon 地址和镜像仓库5.2 避坑清单坑一盲目装插件。Docker Pipeline 插件解决的是用 docker 语法写 Pipeline的问题不是找不到 docker 命令的问题。装它没用别浪费时间。坑二改/etc/profile以为能生效。前面说过非登录 shell 不读这个文件。改了不生效很正常别怀疑人生。坑三用sudo docker。Jenkins 用户默认没 sudo 权限而且就算有sudo 的环境变量继承又是一堆坑。正确做法是把 jenkins 用户加进 docker 组usermod -aG docker jenkins然后重启 Jenkins 服务。坑四忽略 agent 标签。Pipeline 里写了agent any结果任务被调度到一台没装 docker 的 agent 上。用agent { label docker-agent }明确指定别偷懒。坑五socket 权限不对。挂载 socket 后容器里的用户 uid 和宿主机 socket 的属主对不上报 Permission denied。解决办法是让容器用户 uid 和宿主机 docker 组 gid 一致或者简单粗暴chmod 666 /var/run/docker.sock不推荐生产用。实操心得排查这类问题时我习惯在 Pipeline 最前面加一个环境快照阶段把 PATH、which docker、id、docker version 全打出来。虽然多花几秒但能省掉后面几小时的瞎猜。这个习惯是从无数次改了半天发现方向错了里总结出来的。5.3 一个万能诊断脚本把这段存成共享库或者 snippet遇到问题先跑它def call() { sh echo 环境诊断开始 echo [PATH] $PATH echo [USER] $(id) echo [DOCKER BIN] $(which docker 2/dev/null || echo not found) echo [DOCKER VERSION] $(docker version 21 | head -5 || echo cannot run) echo [SOCKET] $(ls -l /var/run/docker.sock 2/dev/null || echo no socket) echo 环境诊断结束 }跑完这一遍问题在哪基本一目了然。比在群里问为什么 docker not found高效得多。6. 从根上避免环境一致性的一些实践6.1 把环境要求写进代码与其每次出问题再排查不如把环境要求固化下来。我的做法是在项目根目录放一个ci-env.md写清楚这个 Pipeline 需要什么环境docker 版本、需要的 PATH、agent 标签、镜像仓库地址。新人接手一看就懂不用口口相传。更进一步用 Dockerfile 定义 agent 镜像把 docker CLI、常用工具全装进去版本锁定。这样环境就是代码可复现、可追溯。6.2 优先选无 daemon 方案如果团队还在用挂 socket 的老方案我建议逐步迁移到 Kaniko 或 Buildah。原因不只是安全还有可移植性——无 daemon 方案在本地、在 CI、在 K8s 里行为一致不会出现本地能跑 CI 跑不了的破事。迁移成本主要在改 Pipeline 脚本一次性投入长期省心。6.3 给 Pipeline 加一层环境断言在 Pipeline 开头加一段断言环境不对直接 fail fast别等到构建到一半才报错stage(Preflight) { steps { sh command -v docker /dev/null 21 || { echo docker not available; exit 1; } docker info /dev/null 21 || { echo docker daemon not reachable; exit 1; } } }这样报错信息清晰定位快也不会浪费构建资源。我个人在实际操作中的体会是docker: not found这个报错本身不复杂复杂的是它背后反映的环境不一致问题。今天你解决了 PATH明天可能又冒出个git: not found、mvn: not found根子都一样。与其一个个打补丁不如花点时间把 agent 环境标准化把该装的装齐、该配的配好后面能省下大量救火时间。最后再分享一个小技巧如果你不确定某个命令在 Jenkins 环境里能不能跑别猜直接在 Pipeline 里sh command -v xxx试一下一秒钟出结果比翻文档快多了。