Docker 基础体系 · 第 62/70 篇。示例以现代 Docker Engine、BuildKit 与 Compose 规范为基础,并明确 Linux 容器边界。
Docker User Namespace Remap:UID 映射、Volume、权限和迁移
Docker 的 User Namespace Remap 是一种 Linux 内核用户命名空间机制:容器内部看到的 UID 仍然可以从 0 开始,但这个 UID 在宿主机上会被映射为一个没有特权的高位 UID。于是,容器内的 root 不再等价于宿主机的 root。
这项能力经常和三个问题同时出现:
- 容器内执行
id显示uid=0(root),为什么宿主机上却是uid=100000? - 为什么启用 remap 后,原来可写的 bind mount 或 Docker Volume 变成了
Permission denied? - 已有镜像、容器和数据如何迁移,才能避免把宿主机目录错误地改成不可恢复的权限状态?
本文讨论的是 rootful Docker Engine 上的 User Namespace Remap,运行边界是 Linux 容器。它与 Rootless Docker 有关联,但不是同一种机制。
一、先区分 User Namespace Remap 与 Rootless Docker
1. User Namespace Remap 是“容器身份降权”
在 rootful Docker 中:
dockerd:宿主机 root 身份运行
容器进程:进入独立的 user namespace
容器 UID 0:映射到宿主机的非特权 UID
典型映射如下:
容器 UID 0 -> 宿主机 UID 100000
容器 UID 1000 -> 宿主机 UID 101000
容器 UID 1001 -> 宿主机 UID 101001
Docker daemon 仍然可能以宿主机 root 身份管理网络、挂载、存储和容器生命周期,但容器中的进程不能再把 UID 0 直接当作宿主机 UID 0 使用。
2. Rootless Docker 是“daemon 本身也不需要宿主机 root”
Rootless Docker 的结构不同:
用户身份运行 dockerd
│
└── dockerd 和容器都运行在用户命名空间中
因此,Rootless Docker 除了 UID/GID 映射,还会影响:
- 网络实现;
- 存储驱动;
- cgroup 能力;
- 设备访问;
- 端口绑定;
- daemon 的系统管理权限。
User Namespace Remap 可以部署在 rootful Docker 上,而 Rootless Docker 不是它的简单开关版本。两者都使用 user namespace,但权限边界和故障路径不同。
二、Linux User Namespace 的核心模型
1. UID 映射不是“改名”,而是内核身份转换
Linux user namespace 为进程提供一组独立的用户和组 ID 视图。
容器进程在 namespace 内部的有效 UID 可能是:
uid=0(root)
但内核访问宿主机文件时,会根据该进程所在 namespace 的映射表,把它转换成宿主机 UID。
对于一个最常见的连续映射区间,可以写成:
其中:
- :容器内 UID;
- :宿主机看到的 UID;
- :该映射区间在宿主机上的起始 UID。
例如:
B = 100000
C = 0
则:
H = 100000 + 0 = 100000
容器 UID 1000 对应:
H = 100000 + 1000 = 101000
GID 映射完全独立,不能因为 UID 映射成立,就假设 GID 也一定成立。
2. 一个完整的映射表
假设 Docker 配置了:
容器 UID 范围:0-65535
宿主机 UID 范围:100000-165535
那么:
| 容器 UID | 宿主机 UID | 容器内通常显示 |
|---|---|---|
| 0 | 100000 | root |
| 1 | 100001 | daemon 等 |
| 1000 | 101000 | 应用用户 |
| 65535 | 165535 | 数字 UID |
容器内部的 /etc/passwd 仍然可能把 UID 1000 显示为某个用户名;这个用户名只属于容器的用户数据库,不代表宿主机上存在同名用户。
3. 映射范围之外的 UID
默认常见的映射范围是 65536 个 UID,但具体范围取决于 /etc/subuid、/etc/subgid 和 Docker 配置。
如果容器需要使用 UID 70000,而映射只覆盖到 65535,则该 UID 没有对应的宿主机 UID。可能出现的表现包括:
- 容器进程无法按预期启动;
- 文件显示为未映射身份;
chown失败;- 应用创建文件时报错;
- 在不同内核和工具组合下显示为
nobody或数字 UID。
因此,镜像中的 UID 设计和 subordinate UID 范围必须相互匹配。
三、Docker 如何配置 User Namespace Remap
1. subordinate UID/GID
Docker 不应任意使用宿主机上的普通用户 UID。它使用 /etc/subuid 和 /etc/subgid 声明一段可分配给 user namespace 的 UID/GID 范围。
典型内容:
dockremap:100000:65536
含义是:
用户名 dockremap
起始 UID 100000
连续数量 65536
对应的 GID 配置:
dockremap:100000:65536
实际起始值不必是 100000,但不能与宿主机真实用户、系统服务或其他 namespace 分配发生冲突。应先检查:
getent passwd dockremap
getent group dockremap
grep '^dockremap:' /etc/subuid
grep '^dockremap:' /etc/subgid
某些发行版或 Docker 安装方式会在启用 default 模式时创建 dockremap 用户和组;显式使用自定义账号时,应由系统管理员预先创建并验证。
2. daemon 配置
可以在 /etc/docker/daemon.json 中配置:
{
"userns-remap": "dockremap"
}
然后重启 Docker:
sudo systemctl restart docker
验证:
docker info | grep -i -A3 -B3 userns
也可以使用:
dockerd --userns-remap=default
default 通常表示使用 Docker 约定的 dockremap 身份。生产环境更适合显式指定账号和映射范围,因为这样便于审计和迁移。
3. 配置生效的范围
userns-remap 是 daemon 级别的默认行为。它会影响该 daemon 创建和管理的容器、镜像存储以及相关文件。
单个容器可以使用:
docker run --userns=host ...
让容器使用宿主机 user namespace,而不是 Docker 默认的 remapped namespace。但这会显著削弱 UID 隔离,应只在明确知道后果、且有其他安全控制时使用。
在 Compose 中通常对应:
services:
app:
image: example/app
userns_mode: "host"
这不是“修复权限”的普通选项,而是改变容器身份边界的安全配置。
四、容器内 UID、宿主机 UID 与文件权限的完整推导
1. 容器内创建文件
假设:
映射起点 B = 100000
容器进程 UID = 1000
容器进程 GID = 1000
进程在挂载目录 /data 中创建:
/data/output.txt
容器内观察:
id
# uid=1000(app) gid=1000(app)
ls -ln /data/output.txt
# -rw-r--r-- 1 1000 1000 ... /data/output.txt
宿主机观察同一个文件:
ls -ln /host/data/output.txt
# -rw-r--r-- 1 101000 101000 ... /host/data/output.txt
映射关系为:
容器 UID 1000 -> 宿主机 UID 101000
容器 GID 1000 -> 宿主机 GID 101000
容器内的 ls 和宿主机上的 ls 看到不同的数字,但它们指向同一 inode 的所有权转换视图。
2. 宿主机文件反向映射到容器
假设 bind mount 对应的宿主机文件是:
UID 0,GID 0,权限 0755
容器内的 root 并不是宿主机 UID 0,而是宿主机 UID 100000。因此,容器内观察这个文件时,通常不会把它视为容器 UID 0 所有。
更关键的是,权限检查发生在内核转换后的身份上:
容器 root
-> 宿主机 UID 100000
-> 访问宿主机 UID 0、权限 0755 的文件
-> 不满足宿主机 owner 写权限
-> EACCES
这就是以下现象的根本原因:
docker exec -it app sh
id
# uid=0(root)
touch /data/test
# touch: /data/test: Permission denied
uid=0 只说明它是该 user namespace 内的 root,不说明它拥有宿主机 root 的文件访问能力。
3. 权限位、ACL 和 SELinux 是另外三层机制
UID 映射只解决“进程身份如何对应宿主机身份”。实际访问还要经过:
- 传统 owner/group/other 权限位;
- POSIX ACL;
- SELinux 等 MAC 策略;
- mount 选项、只读属性、文件系统能力;
- 容器 capability 和 namespace 限制。
例如,即使宿主机目录已经属于 UID 100000,目录也可能因为权限是 0700、ACL 拒绝或 SELinux 标签不正确而无法访问。
umask 也不会改变 UID 映射。它只影响新建文件的初始权限:
最终权限 = 程序请求权限 AND NOT umask
它不会把容器 UID 1000 变成宿主机 UID 1000,也不会自动修复已有文件。
五、Volume、bind mount 与 User Namespace Remap
Docker 中常被统称为“Volume”的挂载,至少要区分以下类型:
bind mount:宿主机指定目录
named volume:Docker 管理的数据卷
tmpfs mount:内存文件系统
它们在 User Namespace Remap 下的权限行为不同。
1. bind mount:权限直接由宿主机目录决定
命令:
docker run --rm \
--user 0:0 \
-v "$PWD/data:/data" \
alpine \
sh -c 'id; touch /data/a'
即使容器内显示:
uid=0(root) gid=0(root)
如果宿主机执行:
mkdir -p data
sudo chown root:root data
sudo chmod 0755 data
那么容器内的 root 实际以宿主机 UID 100000 访问该目录,通常不能创建文件。
正确的修复思路是根据应用的容器 UID 设置宿主机所有权。例如应用以容器 UID 1000 运行:
sudo chown -R 101000:101000 data
或者,如果容器内 root 负责写入:
sudo chown -R 100000:100000 data
这里的 101000 和 100000 只是基于映射起点 100000 的算例,不能不经检查直接复制到所有主机。
2. named volume:数据路径由 daemon 管理
创建和挂载 named volume:
docker volume create app-data
docker run --rm \
-v app-data:/data \
alpine \
sh -c 'id; touch /data/test; ls -ln /data/test'
named volume 的实际目录位于 Docker 的 data root 下,常见位置类似:
/var/lib/docker/volumes/<volume>/_data
但以下事项不能假设:
- data root 一定是
/var/lib/docker; - 所有 volume driver 都使用本地目录;
- volume 创建时一定会自动把目录改成应用需要的 UID;
- 第三方 volume driver 的权限语义与
localdriver 一样。
查看实际信息:
docker volume inspect app-data
docker info --format '{{.DockerRootDir}}'
对于 local volume,可以在停写后检查其挂载点;对于 NFS、云盘或第三方插件,必须按对应驱动的身份和权限模型处理。
3. Docker 的镜像层与 Volume 不是同一组数据
User Namespace Remap 启用后,Docker 需要处理镜像层、容器可写层和 volume 的宿主机所有权。镜像文件在容器内看到的 UID 是镜像元数据中的 UID;宿主机底层存储看到的 UID 可能是映射后的高位 UID。
因此不要把以下两个问题混为一谈:
镜像中 /app 的 owner 是谁?
Volume 中 /data 的 owner 是谁?
例如 Dockerfile:
FROM alpine
RUN adduser -D -u 1000 app
WORKDIR /app
COPY --chown=1000:1000 . /app
USER 1000:1000
CMD ["./server"]
COPY --chown=1000:1000 设置的是镜像内的数字 UID/GID。容器以 UID 1000 运行时,它访问 bind mount 或 volume,宿主机侧对应的是映射后的 UID,而不是宿主机 UID 1000。
六、一个可验证的端到端实验
以下实验需要:
- Linux;
- rootful Docker Engine;
- 已配置
userns-remap; - 使用 local volume 或普通 bind mount;
- 当前账号可以执行 Docker 命令。
先检查映射:
docker info | grep -i -A3 -B3 userns
grep '^dockremap:' /etc/subuid
grep '^dockremap:' /etc/subgid
假设得到:
dockremap:100000:65536
1. 测试容器身份
docker run --rm alpine sh -c '
echo "inside:"
id
touch /tmp/example
ls -ln /tmp/example
'
预期类似:
inside:
uid=0(root) gid=0(root) groups=0(root)
-rw-r--r-- 1 0 0 ... /tmp/example
/tmp/example 位于容器可写层中,容器内显示 UID 0;宿主机底层存储中,其实际 owner 通常是映射后的 UID,而不是宿主机 root。
2. 测试 bind mount
rm -rf ./demo-data
mkdir ./demo-data
chmod 0755 ./demo-data
docker run --rm \
-v "$PWD/demo-data:/data" \
alpine \
sh -c 'id; touch /data/from-container'
如果宿主机目录属于 root:root 且不可由 UID 100000 写入,预期会失败:
touch: /data/from-container: Permission denied
接下来让宿主机目录属于容器 root 的映射 UID:
sudo chown 100000:100000 ./demo-data
docker run --rm \
-v "$PWD/demo-data:/data" \
alpine \
sh -c 'id; touch /data/from-container; ls -ln /data/from-container'
容器内预期看到:
uid=0(root) gid=0(root)
-rw-r--r-- 1 0 0 ... /data/from-container
宿主机上则看到:
ls -ln demo-data/from-container
类似:
-rw-r--r-- 1 100000 100000 ... demo-data/from-container
3. 测试应用 UID 1000
sudo chown -R 101000:101000 ./demo-data
docker run --rm \
--user 1000:1000 \
-v "$PWD/demo-data:/data" \
alpine \
sh -c 'id; touch /data/from-1000; ls -ln /data/from-1000'
容器内显示 UID 1000,宿主机看到 UID 101000。这证明 --user 1000:1000 不会让进程获得宿主机 UID 1000。
七、Compose 中的 UID 与挂载配置
一个典型 Compose 配置:
services:
app:
image: example/app:1.0
user: "1000:1000"
volumes:
- ./data:/var/lib/app
command: ["./server"]
这里有三个独立概念:
user: "1000:1000":设置容器进程的 namespace 内 UID/GID;./data:/var/lib/app:把宿主机目录直接挂载进容器;- daemon 的
userns-remap:决定容器 UID 1000 在宿主机上对应哪个 UID。
在起始 UID 为 100000 时,应用写入 ./data 的文件通常属于宿主机 UID 101000。
宿主机目录可以提前准备:
mkdir -p data
sudo chown 101000:101000 data
如果应用需要多个身份写入,应分别计算对应映射:
容器 UID 1000 -> 宿主机 UID 101000
容器 UID 1001 -> 宿主机 UID 101001
不要只根据容器内用户名设置宿主机 owner。宿主机权限检查使用的是数字 UID/GID;两个系统中即使用户名都叫 app,数字身份也可能完全不同。
八、启用 Remap 时为什么已有 Docker 数据会“消失”
这是迁移中最容易被低估的行为。
Docker 开启 User Namespace Remap 后,daemon 通常会为 remapped 数据使用独立的存储视图。已有的镜像、容器和可写层可能仍然存在于原来的 data root 中,但新配置下的 daemon 不一定继续把它们视为当前状态。
典型表现:
docker ps -a
# 看不到原来的容器
docker images
# 看不到原来的镜像
这不一定意味着文件已经被删除。常见原因是:
旧 daemon 使用未 remap 的存储目录
新 daemon 使用 remapped 的存储目录
Docker 的存储元数据和层数据必须由对应配置下的 daemon 管理,不能简单地把某个目录复制到另一个目录后期待容器自动出现。
启用前应保存:
docker ps -a
docker image ls
docker volume ls
docker inspect <container>
docker volume inspect <volume>
docker network ls
同时备份:
- Compose 文件;
- Dockerfile 和构建上下文;
- 镜像导出文件;
- Volume 数据;
- bind mount 数据;
- daemon 配置;
/etc/subuid和/etc/subgid;- 运行时所需的环境变量与 secret 配置。
1. 不能通过直接改名目录完成迁移
以下做法风险很高:
mv /var/lib/docker /var/lib/docker.old
mv /var/lib/docker.remapped /var/lib/docker
Docker 的 data root 包含内部元数据、存储驱动目录和 daemon 状态。存储驱动、Docker 版本、配置和 UID 归属都可能影响可用性。直接移动目录可能导致:
- 镜像层无法挂载;
- 容器元数据与层不匹配;
- volume 被错误识别;
- daemon 启动失败;
- 文件被错误
chown; - 回滚困难。
正确的迁移方式应以“重新创建容器和镜像,单独迁移数据”为主。
九、Volume 和 bind mount 的迁移方法
1. 先停止写入
无论是 bind mount 还是 named volume,都必须先停止会写入数据的容器:
docker compose down
或者至少停止相关服务:
docker stop app
如果数据库仍在运行,直接复制目录可能得到逻辑上不一致的数据,即使 Unix 权限完全正确,也可能无法恢复。
2. bind mount 的迁移:转换 owner
假设旧系统中,应用以容器 UID 1000 运行,迁移前文件在宿主机上是 UID 1000;新系统设置:
容器 UID 1000 -> 宿主机 UID 101000
则迁移时需要把文件 owner 转换为 101000:
sudo find /srv/app-data -xdev -uid 1000 -exec chown 101000 {} +
sudo find /srv/app-data -xdev -gid 1000 -exec chgrp 101000 {} +
如果旧数据中的容器 root 文件对应宿主机 UID 0,而新容器 root 映射为 100000,则还需要按语义转换:
sudo find /srv/app-data -xdev -uid 0 -exec chown 100000 {} +
sudo find /srv/app-data -xdev -gid 0 -exec chgrp 100000 {} +
这类命令只能在明确知道旧 UID 含义时执行。宿主机 UID 0 可能不仅表示“旧容器 root”,还可能表示宿主机管理员创建的文件。无差别递归替换 UID 可能破坏宿主机共享目录上的其他访问关系。
更安全的流程是:
- 记录旧目录中每个数字 UID/GID 的含义;
- 为新映射建立转换表;
- 只处理属于该应用数据树的文件;
- 先在副本上测试;
- 启动后用应用实际读写验证。
3. named volume 的迁移:使用数据归档,不依赖内部路径
对于 Docker 管理的 local volume,可以使用临时容器进行逻辑复制:
docker volume create app-data-new
docker run --rm \
-v app-data-old:/from:ro \
-v app-data-new:/to \
alpine \
sh -c '
cd /from &&
tar cpf - . | tar xpf - -C /to
'
这里使用 tar 的目的,是保留:
- 相对路径;
- 文件类型;
- 数字 UID/GID;
- 权限位;
- 符号链接;
- 时间戳。
但是,app-data-old 中的数据必须能被当前 remapped 容器读取。如果旧数据在宿主机上属于 UID 0,而当前容器 root 映射为 UID 100000,那么读取可能直接失败。
此时可以采用两种方式:
方式一:在宿主机停 Docker 后由宿主机 root 复制
这适合 local volume,且需要明确 Docker data root 和 volume 路径。先停止 Docker:
sudo systemctl stop docker
再由宿主机 root 复制数据到备份目录或新存储位置,最后按新映射转换 owner。完成后再启动 Docker:
sudo systemctl start docker
不要在 Docker 正在使用 volume 时直接修改其内部目录。
方式二:先让旧数据符合临时迁移身份
如果使用临时容器复制,需要确保读取进程的宿主机映射 UID 对旧数据有读权限,或者由宿主机管理员先在停写状态下调整权限。完成归档后,再根据目标容器 UID 设置目标 volume 中的 owner。
迁移不是简单的“把目录复制过去”。真正需要迁移的是:
应用在容器内期望的 UID/GID
↓
目标 daemon 的映射规则
↓
目标文件系统中的数字 UID/GID
十、镜像迁移与容器重建
镜像应通过镜像级别的方式迁移,而不是复制 Docker 的内部层目录。
导出镜像:
docker save example/app:1.0 | gzip > example-app-1.0.tar.gz
导入镜像:
gunzip -c example-app-1.0.tar.gz | docker load
或者在目标环境中重新构建:
docker buildx build \
--load \
-t example/app:1.0 \
.
Dockerfile 中应明确应用运行 UID:
FROM alpine
RUN addgroup -S -g 1000 app \
&& adduser -S -D -H -u 1000 -G app app
WORKDIR /app
COPY --chown=1000:1000 . /app
USER 1000:1000
ENTRYPOINT ["./server"]
这样做的价值是把“应用身份”固定在镜像中,而不是依赖某台宿主机上的用户名。
但要注意:
USER 1000:1000
只规定容器内身份。它不规定宿主机文件的 owner。宿主机 owner 仍然由 user namespace 映射决定。
十一、启动失败与权限故障的诊断顺序
1. 先确认容器进程身份
docker exec <container> id
docker exec <container> sh -c 'stat -c "%u:%g %a %n" /path/to/file'
确认:
- 进程 UID/GID;
- 文件在容器内看到的 UID/GID;
- 权限位;
- 是否真的以预期用户运行。
2. 再确认宿主机文件身份
对于 bind mount:
stat -c '%u:%g %a %n' /host/path
namei -l /host/path
getfacl /host/path
namei -l 很重要,因为即使最终目录 owner 正确,父目录某一级没有执行权限,也会导致 EACCES。
3. 计算预期映射
如果 /etc/subuid 是:
dockremap:100000:65536
应用以容器 UID 1000 运行,则预期宿主机 UID 是:
100000 + 1000 = 101000
如果宿主机文件属于 UID 1000,而容器实际以 UID 1000 通过 remap 运行,那么它们不是同一个宿主机身份。
4. 检查挂载是否只读
docker inspect <container> \
--format '{{json .Mounts}}' | jq
关注:
{
"RW": false
}
以下配置都会导致写入失败,但原因不是 UID 映射:
-v /host/data:/data:ro
或者 Compose:
volumes:
- ./data:/data:ro
5. 检查 SELinux
在启用 SELinux 的发行版上,Unix owner 正确仍不代表容器可以访问。
Compose 可以使用:
services:
app:
volumes:
- ./data:/data:z
或:
services:
app:
volumes:
- ./data:/data:Z
:z 通常用于多个容器共享的 SELinux relabel,:Z 通常用于私有标签。具体行为取决于 Docker、发行版和 SELinux 配置。
这解决的是 SELinux 标签问题,不是 UID 映射问题。不能用 :z 代替 chown,也不能用 chown 代替 SELinux 标签配置。
6. 检查网络文件系统和 root squash
NFS 等远程文件系统可能根据宿主机传过去的数字 UID 做权限判断;服务端的 root_squash 还可能把 UID 0 映射为匿名用户。
在这种场景中,容器 UID 0 经过 User Namespace Remap 后变成宿主机 UID 100000,再由 NFS 客户端传递给服务端。服务端看到的可能就是 100000,而不是 0。
因此需要同时检查:
- NFS 服务端的 UID/GID;
- export 选项;
- root squash;
- 客户端 mount 参数;
- 容器映射范围。
十二、常见误解与反例
误解一:容器内 root 一定可以 chown
反例:
docker run --rm \
-v "$PWD/data:/data" \
alpine \
sh -c 'chown 1000:1000 /data'
如果 /data 中的文件由宿主机 root 所有,容器 root 实际是宿主机 UID 100000。它不是宿主机 root,可能没有权限改变宿主机 root 所有文件的 owner。
CAP_CHOWN 也不能把 user namespace 中的能力扩展成宿主机 root 权限。capability 的有效范围受到 namespace 限制。
误解二:把宿主机目录 chmod 777 就等于解决问题
chmod 777 可能让普通映射 UID 能够写入,但它会改变数据的安全边界,并不能解决:
- SELinux 拒绝;
- NFS root squash;
- 只读挂载;
- ACL 拒绝;
- 应用要求特定 owner;
- 文件被其他服务共享时的隔离问题。
它是一个权限放宽动作,不是 UID 映射修复。
误解三:user: "1000:1000" 会使用宿主机 UID 1000
在 remap 开启时:
容器 UID 1000 -> 宿主机 UID 101000
user 控制的是容器 namespace 内的身份,不能直接指定宿主机身份。
误解四:Volume 自动替应用修复权限
Docker 有时会在特定挂载和镜像场景中执行目录初始化或权限处理,但不能把这种行为当成所有 volume driver、所有镜像和所有版本下的保证。
应用应通过以下方式明确权限契约:
- 镜像中固定运行 UID/GID;
- 初始化阶段只处理空目录;
- 宿主机或 volume driver 预先设置 owner;
- 对已有数据执行有计划的迁移;
- 启动前通过写入测试验证。
误解五:User Namespace Remap 能阻止所有容器逃逸
它降低了容器 root 对宿主机文件和部分内核资源的直接权限,但不是完整的安全边界。它不能消除:
- Linux 内核漏洞;
- Docker daemon 本身的高权限;
- 不安全的
--privileged; - 宿主机目录主动暴露;
- 设备节点访问;
- 错误的 capability 配置;
- SELinux/AppArmor 配置缺失;
- 运行时或存储驱动漏洞。
容器安全仍然需要结合最小 capability、只读文件系统、设备限制、MAC 策略和及时更新。
十三、并发、状态和故障路径
User Namespace Remap 的权限问题通常发生在以下数据流中:
flowchart LR
A[容器进程\nUID 1000] --> B[容器 user namespace]
B --> C[UID/GID 映射\n1000 -> 101000]
C --> D[Linux VFS 权限检查]
D --> E[宿主机 bind mount 或 volume]
D --> F[POSIX ACL]
D --> G[SELinux/MAC]
D --> H[文件系统或远程存储]
关键路径是:
- 应用选择容器 UID;
- user namespace 把该 UID 映射到宿主机 UID;
- VFS 根据宿主机文件 owner、权限位和 ACL 检查访问;
- SELinux 等强制访问控制继续判断;
- 最终文件系统执行读写。
如果应用启动时执行递归 chown,并且多个副本同时启动,可能出现:
副本 A 正在 chown
副本 B 正在写入
数据库或应用正在读取
这会造成:
- 启动时竞争;
- 大量 inode 操作;
- 文件短暂不可访问;
- 共享 volume 上的 owner 被反复修改;
- 不同版本容器对同一目录使用不同 UID。
因此,权限初始化最好在应用开始服务前完成,并避免多个副本同时对共享数据树执行递归修改。数据库数据尤其不能通过“启动时全目录 chown”作为常规修复手段。
十四、生产迁移的推荐流程
一个可回滚的迁移流程应把身份、镜像和数据分开处理。
步骤一:盘点现状
docker ps -a
docker image ls
docker volume ls
docker info
docker inspect <container>
docker volume inspect <volume>
记录:
- 每个服务实际运行的 UID/GID;
- bind mount 路径;
- named volume;
- volume driver;
- 数据库备份;
- Compose 和镜像版本。
步骤二:规划映射区间
确认:
grep '^dockremap:' /etc/subuid
grep '^dockremap:' /etc/subgid
规划至少覆盖容器会使用的 UID/GID。不要与宿主机账户和其他服务的 subordinate range 重叠。
步骤三:在测试机启用 remap
不要直接在唯一生产 daemon 上试验。先用相同的:
- Docker Engine 大版本;
- daemon 配置;
/etc/subuid;- Compose 文件;
- 镜像;
- 数据备份;
验证:
docker run --rm alpine id
docker run --rm -v /test/data:/data alpine touch /data/test
步骤四:迁移镜像和配置
使用:
docker save
docker load
docker buildx build
或直接从镜像仓库重新拉取,不复制 Docker 内部存储目录。
步骤五:迁移并转换数据 owner
根据应用的容器 UID 计算宿主机映射 UID:
目标宿主机 UID = 映射起点 + 容器 UID
例如:
容器 UID 1000
映射起点 100000
目标宿主机 UID 101000
仅对应用专属目录执行转换,并保留旧数据副本。
步骤六:启动后验证
验证内容应包括:
docker exec <container> id
docker exec <container> sh -c 'touch /data/probe && stat -c "%u:%g %a %n" /data/probe'
stat -c '%u:%g %a %n' /host/data/probe
同时验证:
- 应用能否读已有数据;
- 应用能否创建新文件;
- 应用能否删除和重命名;
- 数据库能否创建锁文件;
- 多容器共享目录是否符合预期;
- 备份程序能否读取;
- SELinux 和 ACL 是否仍然正确。
步骤七:保留回滚路径
回滚至少需要保留:
- 旧 daemon 配置;
- 旧 Docker data root;
- 原始 volume 或备份;
- 原始 owner 映射记录;
- 可重新部署的 Compose 文件;
- 镜像归档。
不要在确认迁移成功前,对唯一一份数据执行不可逆的全量 chown。
十五、与 Rootless Docker 的边界
User Namespace Remap 主要解决:
容器 root 不直接等于宿主机 root
Rootless Docker 还会改变 daemon 的运行身份和系统能力,因此同一个 bind mount 在两种模式下可能出现不同的宿主机 owner:
rootful + userns-remap:
容器 UID 1000 -> remapped subordinate UID
rootless:
容器 UID 1000 -> rootless daemon 用户对应的映射身份
此外,Rootless Docker 常见的存储和网络行为也不同。不能把针对 rootful User Namespace Remap 的 UID 计算直接套到 Rootless Docker 上。
迁移时应把以下内容视为独立变量:
- daemon 是 rootful 还是 rootless;
- 是否启用 daemon 级
userns-remap; - subordinate UID/GID 起始值;
- data root;
- volume driver;
- 容器内应用 UID/GID;
- 宿主机文件系统和 SELinux 配置。
十六、最后的判断标准
User Namespace Remap 是否配置正确,不能只看:
docker exec <container> id
因为容器内显示 uid=0 是预期行为,并不能证明它具有宿主机 root 权限。
完整判断应同时满足:
容器内身份明确
+ UID/GID 映射范围足够
+ bind mount 或 volume 的宿主机 owner 正确
+ 权限位和 ACL 允许访问
+ SELinux/MAC 标签正确
+ volume driver 支持当前身份模型
+ 迁移后的镜像、容器和数据分别经过验证
其中最重要的计算是:
在 dockremap:100000:65536 的示例中:
容器 root -> 宿主机 UID 100000
容器 UID 1000 -> 宿主机 UID 101000
一旦明确这一点,Permission denied、Volume owner 异常、迁移后无法读写和容器内 root 无法 chown 等问题,都可以沿着同一条链路诊断:
容器身份
→ user namespace 映射
→ 宿主机数字 UID/GID
→ 文件权限和 ACL
→ SELinux 与文件系统策略
系列导航与关联阅读
- 系列入口:Docker 完整学习路线:从镜像与容器到安全、可观测和生产交付
- 上一篇:Docker Daemon 配置:data-root、日志、代理、镜像源、TLS 和验证
- 下一篇:Docker Live Restore 与重启恢复:Daemon 故障、容器存活和限制
- 延伸:Rootless Docker:User Namespace、网络、存储、限制和迁移
- 延伸:Docker Volume 权限:UID/GID、umask、Rootless、SELinux 和共享
官方资料
本文依据 Docker、OCI 与 CNCF 官方文档重新梳理;正文与生产检查清单由 WR BLOG 编写。

评论
0 条讨论