news 2026/11/6 14:47:59

多容器共享目录权限踩坑记

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
多容器共享目录权限踩坑记

背景

项目中有算法服务和后端服务,后端服务负责文件上传与保存,算法服务负责文件内容解析、报告生成与保存;采用docker-compose方式部署,
两个服务都会读写同一目录/shared(挂载自宿主机/data/project/share-data)。
上线后发现:后端服务无法成功上传文件,算法服务无法将生成的报告写入目录,服务日志提示权限不足的错误,本文记录排查过程,并给出共享组+SGID+umask的解决方案。

正文

问题现象与环境

权限不足的错误

PermissionError:[Errno13]Permission denied:'/shared/uploads'

docker-compose.yml 挂载配置

# ---- 后端服务 ----backend-app: volumes: - /data/project/share-data:/shared# ---- 算法服务 ----ai-app: volumes: - /data/project/share-data:/shared

Dockerfile

# ---- 算法服务 ----# 创建了 UID 为 10001、名为 appuser 的用户RUNuseradd...--uid10001appuser# 指定后续指令及容器运行时以此用户身份执行USERappuser# ---- 后端服务 ----# 创建了 appuser 的用户和用户组(useradd -r appuser 的 uid是系统分配的,建议使用固定 uid,防止镜像重建时 uid 变更)RUNgroupadd-rappuser&&useradd-r-gappuser-d/app-s/sbin/nologin appuser# 指定后续指令及容器运行时以此用户身份执行USERappuser

排查过程

本着 “尽量改配置、不动两个服务 Dockerfile” 的原则,开始了排查。以下内容均排除 root 场景。
第 1 步:查看宿主机的共享目录的权限。

$ls-l/data/project/ drwxrwxr-x2ubuntu ubuntu40969月1416:21 share-data $idubuntuuid=1000(ubuntu)gid=1000(ubuntu)

目录的属主和属组都是 ubuntu, 权限 775 (属主 rwx,属组 rwx,其他人 r-x)

第 2 步:对比两个容器内的用户身份。

# 后端服务容器内$iduid=999(appuser)gid=999(appuser)groups=999(appuser)# 算法服务容器内$iduid=10001(appuser)gid=10001(appuser)groups=10001(appuser)# 宿主机确认目录属主属组的数字 id$idubuntuuid=1000(ubuntu)gid=1000(ubuntu)

两个容器内的进程 UID 不同,且都不是 root,也不属于 共享目录的属组gid(1000)。

第 3 步:定位根因。
两个容器里的用户 id 都不等于 共享目录的属主 uid(1000) ,也不属于共享目录的属组 gid(1000),作为共享目录的其他用户,其他用户没有 写(w)的权限,而在目录里新建文件 需要目录的 w+x 权限,两个容器都不满足权限需求,所以提示Permission denied。

根因分析: Linux 是怎么判断文件或目录的读写权限的?

前提:先通过路径解析
访问一个文件,内核先逐级解析路径,每一级目录都需要 x 权限:

访问/data/project/share-data/a.txt ├─/需要 x ├─/data需要 x ├─/project 需要 x ├─/share-data需要 x └─/a.txt 需要看文件权限

任何一级目录缺 x,路径解析失败,直接拒绝。

第 1 步:判断进程的 UID 是否是 root

if(进程UID==0){// root 跳过权限检查(除少数特殊情况,如执行位;实际情况还受其他条件的影响) 允许}

注意: root 对文件读写通常无条件允许,但对执行仍需至少一个 x 位。

第 2 步:比较属主权限位(属主位:只有文件属主本人能用)

if(进程有效UID==文件属主UID){使用文件的“属主权限位”# 如果属主权限位不满足读写条件,则拒绝跳到第5步}

第 3 步:比较属组权限位 (属组位:文件属组的所有成员都能用)

if(进程组列表(主组 + 附加组)包含 文件属组 GID){使用文件的“属组权限位”# 如果属组权限位不满足读写条件,则拒绝跳到第5步}

组位是 rw- 还是 r–,决定能不能写

关键:组列表是主组 + 附加组的并集,只要包含文件属组 GID 就算匹配。

第 4 步:比较其他人权限位

否则{使用文件的“其他人权限位”# 如果其他权限位不满足读写条件,则拒绝}

第 5 步:根据操作类型判断权限位
拿到对应的权限位(属主/属组/其他人之一)后,看具体操作:

操作需要的权限位
读文件内容(cat、read )r
写文件内容(echo >>、write)w
执行文件(./run)x
列出目录(ls)目录的 r
进入目录(cd)目录的 x
在目录里建/删/重命名目录的 w + x

只要需要的位在对应的权限段里有,就允许;否则拒绝。

注意:linux内核在容器内外不看用户名,只看 uid/gid 数字。999 ≠ 10001 ≠ 1000。

方案评估与选择

方案做法优点缺点结论
1.统一 uid / gid两个容器用相同 uid/gid 运行,对齐目录属主/属组;或修改目录属主/属组与容器 uid/gid 对齐简单直接两个容器服务都要改 Dockerfile / docker-compose / 启动参数,还可能影响容器内其他文件权限放弃
2.共享组(SGID + 组可写)建共享组,目录属组设为共享组,容器进程加入附加组,并设置 SGID 与组可写权限不动 Dockerfile,只加配置需要调整 docker-compose 启动配置(group_add、umask)采用
3.ACLsetfacl分别给 999、10001 授权只针对 uid 授权,无需改属组和 umask容器 uid 变化时需同步调整 ACL 命令备选
4.放开 other 写权限chmod o+w目录,或chmod -R 777 /data/project/share-data只改一句,其他都不用动a. 安全风险较大;b. 新文件的 other 位仍受 umask 限制为r--,单独用解决不了互写放弃
5.容器以 root 运行两个服务都用 root 用户运行简单省事安全风险极大放弃

综合评估,选择了方案二,只加配置不动镜像

共享组(SGID + 组可写)方案落地

宿主机侧:

# 0. 关键:目录必须先建好再启动容器,否则 Docker 会建一个 root:root 的目录(当前情况下已有该目录)sudomkdir-p/data/project/share-data# 1. 建共享组,固定 GIDgroupadd-g1001fileshare# 2. 共享目录属组改为 1001chown1000:fileshare /data/project/share-data# 1000 可替换为当前登录的系统用户的id# 3. 【易漏,容易踩坑点】历史文件和目录属组一并迁移:SGID 只对"新建"文件生效chgrp-Rfileshare /data/project/share-data# 历史文件补组写位(保留 x 和其他位):umask只对"新建"文件生效find/data/project/share-data-typef-execchmodg+w{}+# 历史子目录补「组写 + SGID 位」:find/data/project/share-data-typed-execchmodg+w,g+s{}+# 4. 目录设 SGID + 组的读写执行权限 rwx: 2775 = 2(setgid) + 775chmod2775/data/project/share-data# 5. 宿主机侧用户放文件时也要保留组写位(umask 022 会导致新文件为 644)(看具体情况设置)echo"umask 002">>~/.profile# 或每次放完后:sudo chmod -R g+w /data/project/share-data# 6. 确认结果(-n 显示数字 uid/gid,避免容器内组名解析不出来)ls-ldn/data/project/share-data# 期望:drwxrwsr-x 1000 1001

容器侧(docker-compose.yml) 调整 group_add 和 umask:

# 后端服务加入共享组,(group-add 只增加进程的附加组身份,不会自动改变目录属组,也不会自动让新建文件拥有组写权限。)backend-app: group_add: -"1001"# 设置 umask 0002,使新建文件属组可写command:sh-c"umask 0002 && exec python app.py"# 算法服务加入共享组ai-app: group_add: -"1001"# 设置 umask 0002,使新建文件属组可写command:sh-c"umask 0002 && exec python algorithm.py"

容器侧(entrypoint/command) 这里和上述 docker-compose command 的配置选其一即可:

umask0002# 必须在 exec 之前设置exec"$@"# exec 保持信号传递与 PID 1 语义

umask 0002 的作用:新文件默认权限666 & ~002 = 664,保留组写位;新目录777 & ~002 = 775。

结果验证

目录: drwxrwsr-x10001001/data/project/share-data ,属组权限 rws 出现了 s说明 sgid生效 文件: -rw-rw-r--9991001a.txt ← 后端服务(uid999)建的文件 文件: -rw-rw-r--100011001b.txt ← 算法服务(uid10001)建的文件

在两个容器的 UID(文件所有者)无法统一的情况下,通过 共享组(gid)的方式,使两个容器进程拥有了公共的身份,设置SGID 目录让目录下的新文件继承目录的属组(说明:新建的子目录不仅继承属组,还会继承 SGID 位,所以共享目录的子层级自动继续生效;普通文件只继承属组,不继承 SGID 位。),设置 umask 让新文件组的权限位有了写的权限,以上共同作用两个容器才都能写入共享目录,操作其中的文件。

步骤作用对象机制解决缺失后果
共享组进程(用户本质上是进程)+目录group_add + chgrp进程有 1001 组身份,目录属组 1001组权限不生效
目录 rwx目录chmod 775 的组位能在目录里建/删文件、进入目录进不了目录或建不了文件
目录 SGID目录chmod g+s新文件属组 = 1001新文件属组 = 创建者主组,对方写不了
umask 002进程umask 0002新文件组位有 w新文件组位 r–,对方只能读

最容易踩坑的几个点

  1. 删除、新建、重命名文件看父目录权限
    删除、新建、重命名文件需要父目录的 w+x 权限,不需要文件本身的权限,新建的文件的权限由基本权限和 umask 决定。
  2. SGID 继承组,不继承完整权限
  • 目录设置 SGID(chmod g+s 或 2775)后,在该目录下新建的文件/子目录会继承该目录的所属组,而不是创建者的主组。但它不会继承目录的完整权限位。
  • 目录里的文件权限还是由新建文件时的默认权限和 umask 综合设置的,组是组,权限是权限,权限是对文件或目录设置的。
  1. umask和 SGID 只影响新建对象
    umask 和SGID只在创建文件/目录时参与计算默认权限,对已有对象没有任何作用,所以当前问题需要对已有的目录/文件批量补一次。
  2. 目录必须有 x 权限
  • 目录的 x 位表示“能否进入/穿越该目录”,是访问目录内任何对象的前提。所以目录新建时的默认权限保留 x,基准权限为 777,实际默认权限由 umask 决定。
  • 只给 r 不给 x:ls dir 可能显示名字,但 ls -l dir、cat dir/file 会失败。
$sudochmod444/data/demo/test $ls-altestls: 无法访问'test/1.txt':权限不够 ls: 无法访问'test/2.txt':权限不够 ls: 无法访问'test/..':权限不够 ls: 无法访问'test/.':权限不够 总计0d????????? ? ? ? ? ?.d????????? ? ? ? ? ?..-????????? ? ? ? ? ?1.txt -????????? ? ? ? ? ?2.txt $cattest/1.txt cat: test/1.txt: 权限不够
  1. 文件和目录的 rwx 含义不同
位对文件对目录
r读取文件内容列出目录中的条目名
w修改文件内容在目录中创建/删除/重命名条目
x执行文件进入/穿越目录,访问目录内对象
  1. echo “hello” > /shared/new.txt 指令中权限如何判断?是文件还是目录的权限?
    执行上述一条命令,写了个新文件,但实际分为两个独立动作,先创建文件,再写文件内容

动作 1:在目录 /shared 里创建一个新目录项 new.txt
→ 权限判断对象:目录 /shared,检查目录有 w+x 权限,才允许创建
动作 2:往 new.txt 这个文件里写入内容 “hello”
→ 权限判断对象:文件 new.txt,检查文件有 w权限,才允许写入

  1. 组的作用对象是谁?
  • 作用对象:用户、目录或文件、进程 ;
  • 通过为用户设置共享组、目录或文件设置共享组,容器内的进程加入共享组,才让容器里的用户或进程操作共享目录,共享的是 gid,容器里的属组靠识别数字匹配生效。

排错速查清单

# 1. 进程到底是谁 —— 容器内执行id# 2. 目录到底归谁 —— 用 -n 看数字,容器内组名常常解析不出来ls-ldn/data/project/share-data# 3. 路径每一级的权限 —— 逐级列出,定位"卡在哪一层"namei-l/data/project/share-data/uploads/a.txt# 4. 有没有 ACL 覆盖 —— 传统 rwx 看着对,但可能被 ACL 拦下getfacl /data/project/share-data# 5. 挂载选项 —— 只读挂载 / nosuid 会让一切配置失效mount|grepshare-data

总结

本次的排查过程也是逐渐发现和学习的过程,当前本次的解决方案也是建立在一些前提下的,如果 docker engine 开启了 userns-remap,或者目录位于 NFS 或者设置了只读挂载,本次共享组的方案是不生效的,本文中的一些内容未详细说明,包括linux的权限模型、ACL设置、方案不生效的原因等还需要探究,下篇博客将为您继续解答,敬请期待!

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/11/6 14:47:45

DeepSeek LeetCode 237. 删除链表中的节点 Java实现

LeetCode 237. 删除链表中的节点 题目描述 有一个单链表的 head,我们想删除它其中的一个节点 node。 给你一个需要删除的节点 node。你将 无法访问 head。 链表的所有值都是 唯一的,并且保证给定的节点 node 不是链表中的最后一个节点。 删除给定的节点。…

作者头像 李华
网站建设 2026/11/6 14:47:08

【AI黑话日日新】Day 054|Synthetic Data(合成数据)

一句话说清:让 AI 模型自己“出题、写答案、编教材”来当别的模型的训练资料,而不是全靠从网上扒真实人类的文字。 1. 它到底在说什么 合成数据(Synthetic Data)指的是由程序或模型“造”出来的数据,而不是从真实世界里发生的事、测出来的数里直接采集的。放到大模型语境…

作者头像 李华
网站建设 2026/11/4 0:26:19

Linux CFS 调度器深度解析:调度时机、选任务与负载均衡

1. 从一个真实场景说起:为什么你的服务器会突然卡顿很多运维和开发同学都遇到过这种情况:一台跑着几十个服务的 Linux 机器,CPU 使用率看起来只有 40%,但业务响应时间却突然飙升,日志里出现大量超时。你登录上去用top一…

作者头像 李华
网站建设 2026/10/20 21:23:47

工程复盘|华丰国际皮革城隔墙项目(项城市)轻质水泥隔墙板商业综合体应用实践与工况分析

工程复盘:华丰国际皮革城隔墙项目装配式轻质水泥隔墙板应用实践 一、项目概况 项目名称:华丰国际皮革城隔墙项目 项目地址:河南省周口市项城市东方大道 130 号 实施单位:漯河市庆林建材商贸有限公司 应用产品:华豫林居…

作者头像 李华
网站建设 2026/11/5 23:25:11

Linux内核hibernation流程解析:从内存快照到ACPI S4恢复

这篇梳理一下Linux内核的hibernation流程,也就是常说的“挂起到磁盘”(suspend-to-disk)。很多人把resume、suspend、hibernate混着叫,实际在内核里这是完全不同的执行路径,涉及进程冻结、内存快照、swap写入、ACPI S4…

作者头像 李华
网站建设 2026/11/6 1:42:54

卷积神经网络内存与计算量解析:参数量、MACs与运行时内存

1. 一个让无数人困惑的经典问题模型文件明明只有几十兆,甚至几兆,为什么一加载到内存里,占用就翻了好几倍?更离谱的是,跑一次推理,内存曲线直接飙到几个G,显存也跟着炸。这个问题我在刚接触深度…

作者头像 李华