Skip to content
Go back

基于 `cturra/docker-ntp` 的 NTP 容器 `chronyd.pid` Permission Denied 问题排查与迁移方案

Edit page

基于 cturra/docker-ntp 的 NTP 容器 chronyd.pid Permission Denied 问题排查与迁移方案

最近在一个 NTP 容器部署中,我们遇到了一个比较典型、但又比较迷惑的问题:同样一份 ymir 配置,在部分节点上可以正常启动,在另一些节点上却会持续重启,日志反复报错:

Fatal error : Could not open /var/run/chrony/chronyd.pid : Permission denied

这个问题一开始很容易被理解成“某些节点权限没配对”或者“宿主机环境脏了”,但继续往下追会发现,真正的问题并不只是某一个目录的 owner 或 mode,而是 cturra/docker-ntp 这条启动链在容器场景下对运行时环境比较敏感。本文把这次问题的背景、排查过程、根因分析和最终方案整理出来。

背景

原始 NTP 服务使用的是 cturra/docker-ntp 镜像,部署方式大致如下:

services:
  ntp:
    image: cturra/ntp:latest
    tmpfs:
      - /etc/chrony:rw,mode=1750
      - /run/chrony:rw,mode=1750
      - /var/lib/chrony:rw,mode=1750
    network_mode: host
    privileged: true

这个镜像的启动逻辑比较简单:

  1. /opt/startup.sh 动态生成 /etc/chrony/chrony.conf
  2. 调整部分运行时目录权限
  3. 通过 /usr/sbin/chronyd -u chrony -d -x 启动 chronyd

从设计意图看,这条链路没有明显问题:容器主进程先以 root 启动,完成配置文件生成和目录准备,再通过 -u chrony 降权运行 chronyd

问题在于,这套设计对 /etc/chrony/run/chrony/var/lib/chrony 的存在方式和权限模型有一组隐含前提,而这些前提在不同节点上并不总是稳定成立。

故障现象

失败节点上的容器日志大致如下:

2026-04-20T07:45:58Z chronyd version 4.6.1 starting (+CMDMON +NTP +REFCLOCK +RTC +PRIVDROP +SCFILTER +SIGND +ASYNCDNS +NTS +SECHASH +IPV6 -DEBUG)
2026-04-20T07:45:58Z Fatal error : Could not open /var/run/chrony/chronyd.pid : Permission denied

在尝试把 pidfile 挪到 /var/lib/chrony/chronyd.pid 时,错误只是跟着路径一起变化:

Fatal error : Could not open /var/lib/chrony/chronyd.pid : Permission denied

这里有两个值得注意的点:

  1. 报错位置会跟着 pidfile 路径变化,说明问题并不局限于 /run/chrony 这一处。
  2. 同样的配置并不是所有节点都会失败,说明问题不太像单纯的静态配置写错,更像是运行时环境敏感型问题。

排查过程

1. 检查镜像里的运行时目录

先直接进入镜像检查最基础的目录状态:

docker run --rm --entrypoint sh hub.fastonetech.com/tools/cturra/ntp:latest -c '
id chrony;
ls -ld /run /var/run /run/chrony /var/run/chrony 2>/dev/null || true
'

结果显示:

继续对照 upstream 的 startup.sh 可以看到,它的逻辑是:

也就是说,它会“修正已有目录”,但不会“保证目录存在”。

2. 检查 tmpfs owner 是否足够

既然镜像本身不保证 /run/chrony 存在,那么最自然的思路就是在 Docker tmpfs 层显式指定 uid=100,gid=101

tmpfs:
  - /etc/chrony:rw,mode=1750
  - /run/chrony:rw,mode=1750,uid=100,gid=101
  - /var/lib/chrony:rw,mode=1750,uid=100,gid=101

为了确认这条思路是否成立,我们又做了一次手工验证:

docker run --rm   --user 100:101   --tmpfs /etc/chrony:rw,mode=1750   --tmpfs /run/chrony:rw,mode=1750,uid=100,gid=101   --tmpfs /var/lib/chrony:rw,mode=1750,uid=100,gid=101   --entrypoint /bin/sh   hub.fastonetech.com/tools/cturra/ntp:latest -ec '
id
stat -Lc "%u:%g %a %n" /run/chrony /var/lib/chrony /etc/chrony
touch /run/chrony/chrony.pid /var/lib/chrony/chrony.pid
ls -ln /run/chrony/chrony.pid /var/lib/chrony/chrony.pid
'

验证结果说明:

这一步很关键,因为它基本排除了“tmpfs owner 完全没生效”这种最简单的解释。

3. 检查是否可以直接让容器以 chrony 用户运行

既然 chrony 用户自己可以写这些目录,那么另一个直觉上的方案就是直接把容器主进程改成:

user: "100:101"

但这个方案很快也被证伪了。实际日志会变成:

/opt/startup.sh: line 69: can't create /etc/chrony/chrony.conf: Permission denied
Fatal error : Not superuser

原因并不复杂:

所以一旦容器主进程本身就不是 root,就会同时撞上两类问题:

  1. 无法按原方式生成 chrony.conf
  2. chronyd -u chrony 因为“不是 superuser”而拒绝启动

这说明:cturra/docker-ntp 的官方启动脚本和直接使用 user: "100:101" 并不兼容

根因分析

1. 这不是单纯的“chmod 不对”

如果问题只是某个目录缺少写权限,那么在给 tmpfs 明确加上 uid=100,gid=101 后,问题通常就应该结束了。

但这里的实际情况是:

这说明问题并不只是静态 DAC 权限,而更像是:

cturra/docker-ntp 这条“root 启动 -> 生成配置 -> chronyd -u chrony 降权”的链路,在不同节点上的表现并不完全稳定

2. 问题点跟着 pidfile 走,而不是跟着某个固定目录走

最初报错是:

Could not open /var/run/chrony/chronyd.pid : Permission denied

当我们把 pidfile 改到 /var/lib/chrony/chronyd.pid 后,错误也一起变成:

Could not open /var/lib/chrony/chronyd.pid : Permission denied

这类现象通常意味着:

3. 为什么同样配置在不同节点表现不一致

这也是整个问题最容易让人误判的地方。

如果只看 ymir 配置,会觉得所有节点都一样;但从实际结果看,cturra/docker-ntp 这条链路对宿主机环境差异比较敏感,常见的影响因素包括:

因此,“有些节点正常、有些节点异常”并不说明配置本身没问题,反而更可能说明镜像的启动模型对节点环境差异过于敏感。

社区里的常见 workaround

查社区资料后,可以看到几类常见 workaround:

  1. 修正 /run/chrony/var/lib/chrony 的 owner/mode
  2. 强制重建容器,避免残留 pid 或旧挂载状态影响:
docker compose up -d --force-recreate
  1. 某些 Ubuntu 宿主机上检查或调整 AppArmor 规则
  2. 直接切换到容器权限模型更清晰的镜像,例如 simonrupf/docker-chronyd

前两种手段在某些节点上可以缓解问题,但并不能保证跨节点一致性;而第 4 种方案更像是“从镜像设计上规避问题”。

解决方案

方案 A:继续修补 cturra/docker-ntp

理论上,继续沿着这条路往下做也不是完全不行,例如:

但这条路的问题也很明显:

如果目标只是“让当前某一批节点先起来”,它可以作为临时方案;但如果目标是“长期稳定运行”,性价比并不高。

方案 B:切换到 simonrupf/docker-chronyd(最终采用)

最终采用的方案是直接切换到 simonrupf/docker-chronyd

原因主要有几点:

  1. 它明确按 chrony:chrony 用户运行,权限模型更一致。
  2. 官方 compose 示例直接要求三个 tmpfs 都带 uid=100,gid=101
  3. 它更明确地处理 /etc/chrony/run/chrony/var/lib/chrony 这几个关键目录。
  4. 它有固定版本 tag,可直接 pin 版本,而不是继续依赖漂移的 latest

最终配置调整为:

services:
  ntp:
    image: simonrupf/chronyd:0.7.1
    container_name: ntp
    restart: always
    read_only: true
    tmpfs:
      - /etc/chrony:rw,mode=1750,uid=100,gid=101
      - /run/chrony:rw,mode=1750,uid=100,gid=101
      - /var/lib/chrony:rw,mode=1750,uid=100,gid=101
    network_mode: host
    cap_add:
      - CAP_NET_BIND_SERVICE
    environment:
      - NTP_SERVERS=ntp1.aliyun.com,ntp2.aliyun.com,ntp3.aliyun.com
      - LOG_LEVEL=0

相对于旧配置,这里有两个明显变化:

一个顺手修掉的小问题:127.0.0.1 不是理想 fallback

排查过程中还顺手发现,原来的 fallback NTP 地址写成了:

127.0.0.1

如果它的语义是“本地离线时钟”,更常见、也更明确的写法其实是:

127.127.1.1

这不是这次 PID 权限问题的直接根因,但既然已经调整 NTP 组件,顺手改掉会更稳妥。

验证方式

镜像切换完成后,可以通过下面几项做基本验证:

docker logs -f ntp
docker exec ntp chronyc tracking
docker exec ntp chronyc sources

重点关注:

  1. 容器是否还会反复重启
  2. chronyc trackingStratum 是否正常
  3. Leap status 是否为 Normal
  4. sources 中是否能看到上游时间源并建立同步关系

小结

这次问题表面上看是:

chronyd 无法打开 PID 文件,像是一个目录权限问题

但继续往下追会发现,更本质的问题其实是:

cturra/docker-ntp 在容器场景下的启动 / 降权 / 运行时目录模型不够稳,对节点运行环境差异比较敏感

这也是为什么同样配置在不同节点上表现不一致。

对于希望在多节点环境中稳定运行 NTP 容器的场景,直接切换到 simonrupf/docker-chronyd,通常比继续在 cturra/docker-ntp 上叠更多 workaround 更像一个长期可维护的方案

参考资料


Edit page
Share this post on:

Previous Post
排查 `flock` 在 NFSv3 上报 `No locks available` 而在 NFSv4 正常的问题
Next Post
Ubuntu 基础 NIS 客户端容器从 18.04 升级到 22.04 后 `getent passwd -s nis` 失效问题排查与兼容方案