在刚开始使用 docker volume 挂载数据卷的时候, 经常出现没有权限的问题.
这里通过遇到的问题来理解 docker 容器用户 uid 的使用, 以及了解容器内外 uid 的映射关系.
遇到的问题
本地有一个 node 的项目需要编译, 采用 docker 来 run NPM install.
- sudo docker run -it --rm --name ryan \
- -v `pwd`:`pwd` \
- -w `pwd` node \
- NPM install --registry=https://registry.npm.taobao.org
可以看到, install 之后, node_modules 文件的权限变成 root 了. 那么, 作为使用者的我们就没有权限去删除这个文件了.
为什么 docker 输出的文件权限会是 root?
原因
Docker 容器运行的时候, 如果没有专门指定 user, 默认以 root 用户运行. 我们的 node 镜像的里没有指定 user.
容器里的执行用户的 id 是 0, 输出文件的权限也是 0.
以下参考 Understanding how uid and gid work in Docker containers
容器共享宿主机的 uid
首先了解 uid,gid 的实现. Linux 内核负责管理 uid 和 gid, 并通过内核级别的系统调用来决定是否通过请求的权限.
比如, 当一个进程尝试去写文件, 内核会检查创建这个进程的的 user 的 uid 和 gid, 来决定这个进程是否有权限修改这个文件.
这里没有使用 username, 而是 uid.
当 docker 容器运行在宿主机上的时候, 仍然只有一个内核. 容器共享宿主机的内核, 所以所有的 uid 和 gid 都受同一个内核来控制.
那为什么我容器里的用户名不一定和宿主内核一样呢? 比如, superset 容器的用户叫做 superset, 而本机没有 superset 这个用户. 这是因为 username 不是 Linux kernel 的一部分. 简单的来说, username 是对 uid 的一个映射.
然而, 权限控制的依据是 uid, 而不是 username.
That's because the username (and group names) that show up in common linux tools aren't part of the kernel, but are managed by external tools (/etc/passwd, LDAP, Kerberos, etc). So, you might see different usernames, but you can't have different privileges for the same uid/gid, even inside different containers
如果不指定 user, 容器内部默认使用 root 用户来运行
我们继续使用 node 镜像, 你可以在查看 Dockerfile. 里面创建了一个
uid 为 1000 的用户 node, 但没指定运行 user.
docker run -d --rm --name ryan node sleep infinity
我执行的用户为 ryan(uid=1000), 让容器后台执行 sleep 程序.
可以看到, 容器外执行 sleep 的进程的用户是 root. 容器内部的用户也是 0(root). 虽然执行 docker run 的用户是 ryan.
也就是说, 我一个普通用户居然可以以 root 的身份去执行一个命令. 看起来挺恐怖的样子.
容器内部用户的权限与外部用户相同
权限是通过 uid 来判断的. 接下来测试, 相同 uid 的用户可以修改归属于这个 uid 的文件.
宿主机有一个用户 ryan:
刚才使用的 node 镜像的 Dockerfile 也定义了 1000 的用户 node:
我们在本地写一个文件 a, 归属用户 ryan
然后, 通过 volume 挂载的方式, 指定运行 user 为 1000, 启动容器 node:
docker run -d --rm --name test -u 1000:1000 -v $(pwd):/tmp node sleep infinity
可以看到, 容器外执行 sleep 的进程, user 是 ryan(另一个 sleep 进行是前面的 root 用户执行的实例, 没删除).
即, docker run -u 可以指定宿主机运行 docker 命令的用户, -u 指定的 uid 就是 docker 实际运行的进程拥有者.
接下来去容器内部, 看看能不能修改挂载的文件.
可以看到, 我们挂载的文件 a 在容器内部显示 owner 是 node, 即 uid=1000 的用户. 并且有权限查看和修改.
然后, 我们写一个文件 b, 在容器内部, 这个 b 自然属于 uid=1000 的 node. 来看看容器外:
同样的, 容器外显示 b 从属于 uid=1000 的用户 ryan, 并且有权限查看和修改.
如此, 可以证明容器内外共享 uid 和对应的权限.
一定要确保容器执行者的权限和挂载数据卷对应
本文最初的问题就是因为容器执行者和挂载数据卷的权限不同. 容器内部运行是 uid=0 的用户, 数据卷从属与 uid=1000 的 ryan. 最终导致容器写入数据卷的文件权限升级为 root, 从而普通用户无法访问.
如果挂载了 root 的文件到容器内部, 而容器内部执行 uid 不是 0, 则报错没有权限. 我在挂载 NPM cache 的时候遇到了这个问题, 于是有了本文.
一个更加明显的 demo
上面的 demo 恰好宿主机器和容器都存在一个 uid=1000 的用户, 于是很和谐的实现了文件权限共享. 接下来测试一个更加明显的 demo.
宿主机器和容器都没有 uid=1111, 我们以 1111 来执行容器:
docker run -d --rm --name demo -u 1111:1111 -v $(pwd):/tmp node sleep infinity
当前数据卷有文件 a 和 dir any_user. 文件 a 归属与 uid=1000, dir any_user 任何人可以写
运行容器, 并以 uid=1111 执行
登录容器内部, 查看数据卷, 发现文件 a 和 dir any_user 都归属于 uid=1000 的 node(uid 映射)
由于容器内部没有 uid=1111 的用户, 所以显示 I have no name!, 没有 username, 没有 home.
在容器内部执行数据卷的写操作, 提示没权限.(因为数据卷的权限是 uid=1000)
在容器内部写入一个文件到公共数据区 (777).
接下来看看容器外的表现:
数据文件确实有被写入, 内容可读
容器写入的文件的权限都是 1111 的 uid. 由于宿主机没有这个用户, 直接显示 uid
查看进程, 可以发现容器的进程也是 1111
即 - u 指定容器内部执行的用户, 以及容器外在宿主机进程的用户, 同样容器写到数据卷的权限也由此指定.
如此, 这个 demo 更容易理解容器内外的 uid 的对应关系. 理解了以后我们挂载数据卷的时候就不会出现权限问题了.
由于安全问题, 通常也是建议不用使用 root 来运行容器的.
参考
Understanding how uid and gid work in Docker containers
理解 docker 容器中的 uid 和 gid
来源: https://www.cnblogs.com/woshimrf/p/understand-docker-uid.html