Docker 基础体系 · 第 44/70 篇。示例以现代 Docker Engine、BuildKit 与 Compose 规范为基础,并明确 Linux 容器边界。
Docker Volume 权限:UID/GID、umask、Rootless、SELinux 和共享
本文讨论 Linux 容器中的权限。这里的“Volume”既包括 Docker 管理的 named volume,也包括 bind mount;两者都把宿主机或 Docker 存储中的目录挂载到容器路径,但权限来源和生命周期并不相同。Windows 容器的权限模型、Windows ACL 和 Linux UID/GID 映射不在本文范围内。
1. 先建立权限模型:容器内的 UID/GID 不是用户名
Linux 文件权限首先看数字身份,而不是用户名字符串。
一个进程至少具有:
- 有效 UID(effective UID):访问文件时用于判断“文件所有者”的数字身份。
- 有效 GID(effective GID):访问文件时用于判断“文件所属组”的数字身份。
- 补充组(supplementary groups):进程额外加入的组,用于判断组权限。
- 能力(capabilities):将传统 root 权限拆分后的特权集合。
- 用户命名空间映射(user namespace mapping):把容器内 UID/GID 映射到宿主机 UID/GID。
镜像中的 /etc/passwd 和 /etc/group 只负责把数字 ID 显示成名字。例如:
容器内:uid=1000(app) gid=1000(app)
宿主机:uid=1000(alice) gid=1000(alice)
这两个 app 和 alice 没有任何身份关联。对于普通 Unix 权限检查,重要的是数字 1000。
因此,以下两个进程对同一个 bind mount 的访问结果可能完全不同:
容器 A:uid=1000 gid=1000
容器 B:uid=2000 gid=2000
即使两个容器内都显示用户名 app,宿主机上的文件仍然会把它们视为不同用户。
1.1 文件权限的选择顺序
假设文件的元数据为:
-rw-r----- 1 1000 2000 report.txt
权限位可以抽象为:
owner:rw-
group:r--
other:---
Linux 判断某个进程是否可读时,大致按以下顺序选择一组权限:
- 如果进程有效 UID 等于文件 UID,只检查 owner 权限;
- 否则,如果有效 GID 或补充组包含文件 GID,只检查 group 权限;
- 否则检查 other 权限。
重要边界是:命中 owner 后,不会因为 owner 权限不足而回退到 group 权限。
例如:
文件:-rw-r----- 1 1000 2000 report.txt
进程:uid=1000 gid=2000
虽然进程也属于 GID 2000,但它已经命中了 owner 身份;此时 owner 权限为 rw-,不会使用 group 权限。
目录权限还需要特别注意:
r:能列出目录项名称;w:能创建、删除和重命名目录项;x:能穿过目录并访问其中已知名称的文件。
所以,一个进程可能能够读取某个已知路径下的文件,却不能列出父目录;也可能能够列目录但不能打开其中的文件。
1.2 root 并不等于绕过所有检查
传统 root 通常可以绕过大多数 Unix DAC(Discretionary Access Control,自主访问控制)限制,但这不是 Docker 权限的完整定义。
访问还可能被以下机制拒绝:
- Linux capabilities;
- 用户命名空间映射;
- 只读挂载;
- SELinux 等 LSM(Linux Security Module);
- ACL;
- NFS 服务端权限和 root squash;
- 文件系统本身的限制。
因此,“容器里是 root”至少有三种不同含义:
- 容器内 UID 数字为
0; - 该 UID 在宿主机上也映射为真正的 root;
- 该进程拥有足够能力,且没有被 SELinux、文件系统或远端服务拒绝。
只有在传统 rootful Docker 且未受到其他限制时,这三者才比较接近。
2. Docker 挂载类型决定权限的来源
2.1 Bind mount:直接使用宿主机路径
mkdir -p "$PWD/data"
docker run --rm \
--mount type=bind,src="$PWD/data",dst=/data \
alpine sh -c 'id; touch /data/created.txt; ls -ln /data'
如果容器默认进程是 rootful Docker 中的 UID 0,created.txt 通常会在宿主机上显示为 UID 0、GID 0。这里的“通常”是因为实际结果仍受 rootless、用户命名空间、挂载选项和 SELinux 影响。
bind mount 的核心特点是:
- 容器访问的是指定的宿主机目录;
- 宿主机原有文件的 UID/GID 和 mode 直接可见;
- 容器创建或修改文件,会直接改变宿主机文件;
- 宿主机目录不会因为镜像中同名路径有内容而自动填充;
- 挂载会遮挡容器镜像中目标路径原有内容。
最后一点经常造成误判:
docker run --rm \
--mount type=bind,src="$PWD/empty",dst=/etc/example \
image-that-contains-example-config
容器中 /etc/example 显示的是宿主机 empty 的内容,而不是镜像原本的内容。卸载挂载后,镜像文件仍然存在,但在容器运行期间不可见。
2.2 Named volume:由 Docker 管理生命周期
创建并使用 named volume:
docker volume create app-data
docker run --rm \
--mount type=volume,src=app-data,dst=/data \
alpine sh -c 'id; touch /data/file; ls -ln /data'
named volume 的数据由 Docker volume 管理。对于默认的 local volume,数据最终仍然位于宿主机文件系统中,但应用不应依赖某个固定宿主机路径,也不应在 volume 正被使用时直接绕过 Docker 去修改该目录。
named volume 首次挂载到一个空 volume时,Docker 通常会把镜像中目标路径已有的内容复制到 volume 中;复制时会保留文件的基本属性,包括常见的 UID/GID 和 mode。若使用 volume-nocopy,则禁止这次初始化复制:
docker run --rm \
--mount type=volume,src=app-data,dst=/data,volume-nocopy \
image-name
这不是“修复权限”的机制。它只控制初始化时是否复制镜像目录内容。
一个常见现象是:
镜像中的 /var/lib/app 属于 root:root
容器第一次挂载空 volume 到 /var/lib/app
volume 中的数据也因此属于 root:root
应用随后以 UID 1000 启动
结果:Permission denied
更可靠的做法是让镜像构建阶段就创建正确的目录,或由明确的初始化步骤执行 chown,而不是把权限修复寄托在 volume 自动初始化上。
2.3 tmpfs:内存中的临时文件系统
docker run --rm \
--tmpfs /run/app:rw,nosuid,nodev \
alpine sh -c 'mount | grep /run/app; touch /run/app/x'
tmpfs 的数据不进入 named volume,也不会持久化到宿主机普通目录。它仍然使用 Linux UID/GID、mode、umask 和 SELinux 等权限机制,但生命周期是容器或挂载生命周期。
因此,tmpfs 常用于:
- 临时 socket;
- PID 文件;
- 短生命周期缓存;
- 不希望写入持久化存储的中间数据。
它不是解决 UID/GID 不匹配的替代方案。
3. 镜像用户、运行时用户和 volume 初始化
一个镜像可以声明:
FROM alpine
RUN addgroup -S -g 1000 app \
&& adduser -S -D -u 1000 -G app app \
&& mkdir -p /var/lib/app \
&& chown -R 1000:1000 /var/lib/app
USER 1000:1000
VOLUME ["/var/lib/app"]
运行时可以再次覆盖用户:
docker run --rm \
--user 2000:2000 \
--mount type=volume,src=app-data,dst=/var/lib/app \
image-name
此时镜像中的 USER 1000:1000 不再决定最终进程身份;--user 优先级更高。Compose 中对应的是:
services:
app:
image: example/app
user: "1000:1000"
USER app 和 user: "1000:1000" 的区别在于:
USER app依赖镜像内/etc/passwd能解析app;user: "1000:1000"直接使用数字身份,不要求容器内存在同名用户;- 宿主机权限判断最终仍基于数字 UID/GID。
3.1 一个可运行的权限失败示例
docker volume create demo-perm
docker run --rm \
--mount type=volume,src=demo-perm,dst=/data \
alpine sh -c 'mkdir -p /data/owned-by-root; chown 0:0 /data/owned-by-root; chmod 700 /data/owned-by-root'
docker run --rm \
--user 1000:1000 \
--mount type=volume,src=demo-perm,dst=/data \
alpine sh -c 'id; touch /data/owned-by-root/x'
第二个容器通常输出:
uid=1000 gid=1000
touch: /data/owned-by-root/x: Permission denied
原因可以逐步展开:
- 目录所有者是 UID 0;
- mode 是
0700; - 访问者 UID 是 1000,不命中 owner;
- GID 也不匹配;
- other 权限为
---; - 创建文件需要目录的
w和x,两者都不存在。
如果初始化容器执行:
docker run --rm \
--mount type=volume,src=demo-perm,dst=/data \
alpine sh -c 'chown -R 1000:1000 /data'
之后,UID 1000 的容器就可能成功写入。但这个方案有两个边界:
- 初始化容器必须拥有足够权限;
chown -R可能修改大量已有数据,甚至破坏应用需要保留的所有权关系。
在 rootless Docker 中,第二个边界尤其重要,后文会说明。
4. umask:决定新建对象的默认权限,不修复已有文件
umask 是进程创建文件或目录时从请求权限中屏蔽权限位的掩码。它只影响新建对象,不能修改已有文件,也不能授予原本没有的权限。
对常见创建请求,计算关系可以写成:
最终 mode = 请求 mode & ~umask
其中:
请求 mode是程序传给open(O_CREAT)、mkdir等系统调用的 mode;umask是进程的权限屏蔽位;~umask表示取反后保留允许的位;最终 mode是文件系统创建对象时使用的初始 mode。
例如,默认 umask 为 022:
4.1 文件示例
普通程序创建文件时通常请求 0666,因为文件默认不应带执行位:
0666
& ~0022
= 0644
结果是:
-rw-r--r--
4.2 目录示例
程序创建目录时通常请求 0777:
0777
& ~0022
= 0755
结果是:
drwxr-xr-x
如果共享目录需要组成员共同写入,常见目标是 umask 0002:
文件:0666 & ~0002 = 0664
目录:0777 & ~0002 = 0775
这能保留 group 写权限,但前提是应用确实请求了 0666 或 0777。如果应用显式请求 0600,umask 不能把它“放宽”为组可读写。
4.3 容器中的 umask 来源
Docker 没有一个通用的 docker run --umask 参数来替所有应用设置 umask。umask 属于进程状态,通常由以下位置决定:
- 进程自身调用
umask(2); - entrypoint shell 执行
umask 0002; - shell 或启动器为子进程设置的环境;
- 某些运行时或服务管理器的默认行为。
例如:
FROM alpine
RUN addgroup -S -g 1000 app \
&& adduser -S -D -u 1000 -G app app
USER 1000:1000
CMD ["sh", "-c", "umask 0002; exec sh"]
验证:
docker run --rm image-name sh -c '
umask
touch /tmp/a
mkdir /tmp/d
stat -c "%a %U:%G %n" /tmp/a /tmp/d
'
预期结果类似:
0002
664 app app /tmp/a
775 app app /tmp/d
具体用户名可能因镜像内容不同而显示为数字。
需要区分两个经常被混淆的事实:
umask影响“创建时的默认 mode”;- UID/GID 和目录 mode 决定“当前是否能访问”。
如果 volume 中已有一个 0600、所有者为 UID 1000 的文件,把容器 umask 改成 0002 不会改变它。需要显式使用 chmod、chown 或 ACL。
5. 多容器共享:挂载共享不等于协作安全
多个容器可以挂载同一个 named volume:
docker volume create shared-data
docker run -d --name writer \
--mount type=volume,src=shared-data,dst=/data \
alpine sh -c 'while :; do date >> /data/events.log; sleep 1; done'
docker run --rm \
--mount type=volume,src=shared-data,dst=/data,readonly \
alpine tail -n 5 /data/events.log
这里有三层不同问题。
5.1 能不能访问:由 UID/GID、mode、ACL 和 LSM 决定
如果 writer 以 UID 1000 创建:
-rw-r--r-- 1 1000 1000 events.log
另一个容器以 UID 2000 运行时,只能按 other 权限读取。如果文件是 0600,即使两个容器挂载了同一个 volume,第二个容器也无法读取。
共享身份通常需要以下条件同时成立:
访问者 UID/GID 与文件所有权或 ACL 匹配
且目录具有必要的 x/w/r 权限
且挂载不是只读
且 SELinux 等 LSM 允许访问
--group-add 可以加入补充组:
docker run --rm \
--user 1000:1000 \
--group-add 2000 \
--mount type=volume,src=shared-data,dst=/data \
alpine id
如果文件所属组为 GID 2000,且 group 权限允许访问,这个补充组才有意义。
5.2 创建后是否保持共享:需要 setgid 和合适的 umask
假设共享目录属于 GID 2000:
mkdir -p /srv/shared
chown 1000:2000 /srv/shared
chmod 2770 /srv/shared
这里的 2 是 setgid 目录位。它使得在目录中创建的新文件和子目录通常继承目录的组 GID 2000,而不是完全依赖创建者的主组。
容器内进程还需要使用组可写的 umask,例如:
umask 0002
目录请求 0777 -> 0775
文件请求 0666 -> 0664
一个典型共享条件是:
共享目录:GID 2000,mode 2770
容器进程:属于补充组 2000
进程 umask:0002
应用创建文件:不强制使用 0600
如果应用显式执行:
open("x", O_CREAT, 0600)
那么 setgid 和 umask 都不能把文件变成组可写。应用的创建策略仍然是决定因素。
5.3 能不能并发正确写入:需要应用层协议
Docker volume 不提供通用的跨容器事务、租约或文件锁服务。两个进程同时写同一个文件可能产生:
- 追加内容交错;
- 一个进程覆盖另一个进程的更新;
- 临时文件重命名导致观察到不同版本;
- 应用崩溃后留下半写入文件;
- SQLite、日志、索引等数据格式损坏。
Linux 的 O_APPEND、rename()、flock() 等原语在特定条件下有用,但不能把任意“读—改—写”操作自动变成事务。对于数据库,通常应让数据库服务独占其数据目录;对于日志,应使用明确的日志聚合或单写者模型。
所以,以下两个命题完全不同:
两个容器都能访问 /data
两个容器可以安全地并发修改 /data/state.db
前者是权限和挂载问题,后者是应用并发一致性问题。
6. Rootless Docker:容器 root 和宿主机 root 分离
Rootless Docker 让 Docker daemon、容器进程和相关存储操作都尽量以普通宿主机用户运行。它依赖 Linux user namespace,把容器内的 UID/GID 映射到宿主机的 UID/GID 范围。
概念上可以表示为:
容器 UID 0 -> 宿主机启动 Docker 的用户 UID
容器 UID 1 -> 宿主机的 subordinate UID 范围中的某个 UID
容器 UID 1000 -> 另一个映射后的宿主机 UID
实际映射不是固定公式,必须以当前系统的 user namespace 映射为准。相关配置通常来自 /etc/subuid 和 /etc/subgid,但具体分配由系统和 Rootless Docker 配置决定。
6.1 Rootless 下 bind mount 的直接后果
假设宿主机目录:
mkdir -p "$PWD/data"
chmod 700 "$PWD/data"
它属于宿主机用户 alice,并且只有 alice 可以访问。Rootless daemon 通常以 alice 身份运行,因此容器内的 rootless UID 0 可能能够访问该目录。
但如果目录属于另一个宿主机用户:
sudo chown 2000:2000 "$PWD/data"
sudo chmod 700 "$PWD/data"
那么容器内的 rootless UID 0 通常不能凭空绕过宿主机的 DAC。容器内显示:
uid=0(root)
并不表示它是宿主机 UID 0。
这正是 rootless 的安全边界:容器内 root 的权限被用户命名空间限制在宿主机普通用户和其映射范围内。
6.2 Rootless 下 named volume 的所有权观察
rootful Docker 中,volume 内由容器 UID 1000 创建的文件,宿主机上通常也能直接观察到 UID 1000。
Rootless Docker 中,容器 UID 1000 可能映射到 subordinate UID。于是宿主机工具可能显示一个看起来陌生的数字 UID,甚至显示为没有名称的用户。
不要根据“宿主机看到的 UID”直接反推容器内 UID。应在容器内验证:
docker run --rm \
--mount type=volume,src=shared-data,dst=/data \
alpine sh -c 'id; stat -c "%u:%g %a %n" /data'
也可以检查 Rootless Docker 使用的 daemon 数据目录和 namespace 映射,但不应假定 volume 一定位于某个固定路径,更不应在 Docker 正在使用时直接对内部存储目录执行 chown -R 或移动文件。
6.3 Rootless 下 chown 失败的原因
在 rootful 容器中,初始化脚本经常这样写:
chown -R 1000:1000 /data
在 rootless 环境中,该操作可能失败,原因包括:
- 目标 UID/GID 不在当前 user namespace 的映射范围内;
- 宿主机 bind mount 的所有权不允许当前用户修改;
- 远端文件系统不支持所需的所有权操作;
- NFS 服务端使用 root squash 或拒绝客户端的变更。
因此,初始化脚本不应无条件把 chown -R 当作启动前提。更稳妥的设计是:
- 在镜像构建阶段创建正确的应用用户和默认目录;
- 让数据目录由预先准备好的宿主机 UID/GID 管理;
- 只有确认当前身份能修改目标目录时才执行修复;
- 将修复失败作为明确错误,而不是静默继续运行。
6.4 Rootless 与远端存储
Docker 的 volume driver 或底层文件系统可能把权限判断放到 Docker 主机之外。例如 NFS 会由服务端根据客户端发送的 UID/GID、导出选项和 root squash 决定结果。
这会造成一个重要边界:
容器内看到的 UID/GID
≠ 必然是 NFS 服务端认可的身份
所以,rootless、NFS、ACL、Kerberos 或其他远端身份系统组合时,必须同时验证:
- Docker 主机上挂载是否成功;
- Docker daemon 用户是否能访问;
- 服务端导出策略是否允许;
- 容器进程的映射身份是否能读写;
- 文件锁语义是否满足应用要求。
7. SELinux:Unix 权限通过后仍可能被拒绝
SELinux 是 Linux 的强制访问控制系统。传统 UID/GID 和 mode 属于 DAC;SELinux 根据进程和文件的安全上下文进行额外检查。
简化后的访问路径可以表示为:
flowchart TD
A[容器进程发起 open/read/write] --> B[挂载与只读检查]
B --> C[UID/GID、mode、ACL 进行 DAC 判断]
C -->|通过| D[SELinux 等 LSM 判断]
C -->|拒绝| E[Permission denied]
D -->|允许| F[文件系统操作]
D -->|拒绝| G[Permission denied 或 AVC 日志]
图中的顺序是帮助理解的简化模型;实际内核访问路径会涉及多个 hook 和文件系统细节。关键事实是:拥有正确 UID/GID 不保证 SELinux 一定允许访问。
7.1 Bind mount 的 :z 和 :Z
在启用 SELinux 的系统上,Docker 可以为 bind mount 调整文件的 SELinux label:
docker run --rm \
-v "$PWD/data:/data:Z" \
alpine sh -c 'touch /data/test'
常见语义是:
:z:将内容标记为可被多个容器共享的 SELinux 类型;:Z:将内容标记为某个容器私有使用的类型。
它们不是 Unix chmod 的缩写,也不会改变 UID/GID。
例如,宿主机目录可能是:
UID/GID:1000:1000
mode:0770
即使这些 DAC 条件允许容器访问,错误的 SELinux label 仍可能导致:
touch: /data/test: Permission denied
使用 :z 或 :Z 的风险在于它们会修改宿主机路径的安全上下文。对一个包含多个应用数据的宽泛目录执行 :Z,可能破坏其他服务对该目录的预期。生产环境中应只对明确的应用目录进行 relabel,并确认该路径可以接受这种变更。
7.2 SELinux 故障的诊断
先区分普通 DAC 失败和 SELinux 失败:
ls -ldZ ./data
ls -lZ ./data
在宿主机查看拒绝日志:
sudo ausearch -m avc -ts recent
某些发行版也可使用:
sudo journalctl -k | grep -i avc
诊断时应先确认:
- 容器挂载点是否正确;
- 容器内
id是否是预期身份; stat显示的 UID/GID 和 mode;- 宿主机路径的 SELinux context;
- 是否使用了
:z或:Z; - 宿主机是否真的启用了 SELinux。
临时关闭 SELinux 或把系统切换到宽松模式可以用于定位,但不应作为权限修复方案,因为这会改变整台主机的安全边界。
8. readonly、文件系统挂载和“只读”的边界
容器挂载可以设置为只读:
docker run --rm \
--mount type=volume,src=shared-data,dst=/data,readonly \
alpine sh -c 'touch /data/x'
预期会失败,常见错误类似:
touch: /data/x: Read-only file system
这与普通 Unix 权限失败不同:
Permission denied
只读挂载是挂载层面的限制;它不是把文件 mode 改成 0444,也不会修改 volume 中原有文件权限。其他容器如果以读写方式挂载同一个 volume,仍可能修改数据。
同样,容器内执行 chmod 或 chown 的结果也取决于实际挂载和身份:
- 文件系统只读时,通常会失败;
- 进程不是所有者且没有相应能力时,会失败;
- rootless 映射不包含目标 ID 时,可能失败;
- NFS 等远端文件系统可能依据服务端策略失败。
9. 一个可复用的 Compose 共享示例
下面的 Compose 配置演示两个服务以相同数字用户和共享组访问同一个 named volume:
services:
writer:
image: alpine:3.20
user: "1000:1000"
group_add:
- "2000"
command:
- sh
- -c
- |
umask 0002
mkdir -p /data/logs
while true; do
date >> /data/logs/app.log
sleep 5
done
volumes:
- shared-data:/data
reader:
image: alpine:3.20
user: "1001:1001"
group_add:
- "2000"
command:
- sh
- -c
- |
umask 0002
while true; do
tail -n 5 /data/logs/app.log 2>/dev/null || true
sleep 5
done
volumes:
- shared-data:/data
volumes:
shared-data:
这个配置不能单独保证成功,因为 named volume 第一次创建时,其根目录的所有权和 mode 仍可能不符合预期。可以先使用一次性初始化容器设置目录:
docker compose run --rm \
--user 0:0 \
writer sh -c '
mkdir -p /data/logs
chown -R 1000:2000 /data
chmod 2770 /data /data/logs
'
然后启动服务:
docker compose up
其成立条件是:
- 初始化容器的 UID 0 有权修改该 volume;
/data/logs的组为 2000;- 目录 mode 含有 setgid 和组写权限;
- writer 和 reader 都通过
group_add加入 GID 2000; - 应用实际创建的文件没有被强制设为过于严格的 mode;
- Docker 运行环境没有额外的 SELinux 或远端存储限制。
在 rootless Docker 中,第 1 步不一定成立。若 volume 中的目录由 rootless daemon 管理,容器内 UID 0 仍然只是 namespace 内的 root,不能把它当作宿主机 root 使用。
10. 诊断权限问题的固定路径
面对 Permission denied,不要首先修改成 chmod 777。应逐层确认。
10.1 确认挂载对象
docker inspect container-name \
--format '{{json .Mounts}}'
重点检查:
Type是bind、volume还是tmpfs;Source是否为预期路径或 volume;Destination是否覆盖了镜像原有目录;RW是否为true;- 是否设置了 SELinux 相关选项。
10.2 确认容器进程身份
docker exec container-name id
docker exec container-name sh -c 'cat /proc/self/status | grep -E "Uid|Gid|Groups"'
确认:
- 有效 UID;
- 有效 GID;
- 补充组;
- 是否与文件的数字 UID/GID 匹配。
10.3 查看文件而不是只看目录
docker exec container-name \
stat -c 'mode=%A numeric=%a uid=%u gid=%g path=%n' /data /data/file
docker exec container-name \
namei -l /data/file
namei -l 可以逐级显示路径组件权限。很多故障不是文件本身的问题,而是父目录缺少 x,导致无法穿过目录。
10.4 分别测试读、写、创建和删除
docker exec container-name sh -c '
test -r /data/file; echo "read=$?"
test -w /data; echo "dir-write=$?"
test -x /data; echo "dir-traverse=$?"
touch /data/.perm-test
rm /data/.perm-test
'
“能写文件”与“能删除文件”并不等价:
- 修改文件内容主要看文件自身写权限;
- 删除目录项主要看父目录的写和执行权限。
10.5 确认宿主机和 SELinux
对于 bind mount:
stat -c 'mode=%A numeric=%a uid=%u gid=%g path=%n' ./data
ls -ldZ ./data
对于 named volume,可以查看逻辑信息:
docker volume inspect app-data
不建议直接依赖 inspect 输出的宿主机内部路径进行生产操作,尤其是 Rootless Docker、不同存储驱动和远端 volume driver 环境。
如果 DAC 看起来都正确,而错误仍是 Permission denied,检查 AVC 日志和挂载标签。
11. 备份与恢复时,必须保留或重建所有权
权限问题经常在恢复后重新出现。只复制文件内容而不保留 UID/GID、mode、ACL 或 SELinux context,可能让恢复的数据无法被应用使用。
一个简单的 named volume 备份示例:
mkdir -p backup
docker run --rm \
--mount type=volume,src=app-data,dst=/data,readonly \
--mount type=bind,src="$PWD/backup",dst=/backup \
alpine \
tar czpf /backup/app-data.tar.gz -C /data .
这里:
- 第一个挂载以只读方式访问 volume;
- 第二个挂载把宿主机
backup目录提供给临时容器; tar的p选项要求保留权限信息;- 该命令仍需要考虑容器内 tar 的能力、ACL、xattr 和 SELinux context 是否被完整保存。
恢复时:
docker run --rm \
--mount type=volume,src=app-data,dst=/data \
--mount type=bind,src="$PWD/backup",dst=/backup \
alpine \
tar xzpf /backup/app-data.tar.gz -C /data
恢复前应停止会修改该 volume 的应用,避免产生并发写入。否则即使 tar 成功,也可能得到时间点不一致的数据集。
如果目标环境的 UID/GID 与备份来源不同,恢复后有两种策略:
- 保留原数字 UID/GID,并在目标环境创建相应用户或组;
- 恢复后明确执行一次经过验证的 UID/GID 转换。
不要只根据用户名转换,因为用户名在两个镜像或两个主机上可能对应不同数字 ID。
12. 常见误解与反例
误解一:容器里创建 app 用户,宿主机就认识这个用户
反例:
容器:app -> UID 1000
宿主机:alice -> UID 1000
这两个用户名称不同,但普通文件权限判断仍可能允许访问,因为数字 UID 相同。反过来,即使名称都叫 app,只要数字 UID 不同,也可能无法访问。
误解二:umask 0002 能让已有文件变成组可写
反例:
已有文件:0600
修改 umask:0002
文件仍然是 0600。umask 只在创建时参与计算,不能改变已有 mode。
误解三:容器内 root 一定能 chown
在 Rootless Docker、bind mount 到无权目录、NFS root squash 或不支持该操作的文件系统上,容器内 UID 0 可能无法完成 chown。
误解四:共享 volume 会自动提供锁和事务
volume 只提供共享的文件系统视图。它不会自动解决多个应用同时更新同一个状态文件产生的数据竞争。
误解五:chmod 777 可以解决所有 Docker volume 问题
它无法解决:
- SELinux label 拒绝;
- 只读挂载;
- rootless 用户命名空间限制;
- NFS 服务端拒绝;
- 应用并发写入;
- 父目录不可穿越;
- 文件系统错误或配额限制。
它还会扩大不必要的写入范围,增加同机其他进程修改数据的风险。
13. 权限设计的核心边界
可以把 Docker volume 的可写条件概括为:
进程能够写入路径
⇔
挂载层允许写入
且路径中每一级目录可穿越
且目标操作所需的 UID/GID、mode、ACL 条件满足
且 rootless 映射允许该身份执行操作
且 SELinux 等 LSM 允许
且底层文件系统或远端服务允许
对于新建文件,还要额外考虑:
新文件 mode = 应用请求 mode & ~umask
对于多容器共享,还要再增加:
权限可访问
≠
并发语义正确
工程上真正需要先确定的是数据目录的身份契约:
哪个数字 UID 写入?
哪个数字 GID 共享?
新文件需要哪些 mode?
是否需要 setgid 或 ACL?
是否运行在 rootless?
是否启用 SELinux?
是否是本地文件系统还是远端存储?
是否允许多个进程并发修改同一数据结构?
只有这些条件同时明确,USER、--user、group_add、umask、初始化步骤和 volume 挂载选项才会形成可验证的权限方案,而不是靠反复尝试 chmod 和 chown。
系列导航与关联阅读
- 系列入口:Docker 完整学习路线:从镜像与容器到安全、可观测和生产交付
- 上一篇:Docker Storage Driver:overlay2、Copy-on-Write、Inode 和磁盘诊断
- 下一篇:Docker Bind Mount 深入:传播、递归、只读、符号链接和平台差异
- 延伸:Docker 存储:Volume、Bind Mount、tmpfs、权限和备份恢复
- 延伸:Rootless Docker:User Namespace、网络、存储、限制和迁移
官方资料
本文依据 Docker、OCI 与 CNCF 官方文档重新梳理;正文与生产检查清单由 WR BLOG 编写。

评论
0 条讨论