Docker之文件权限问题
前言
在非 root 权限使用 Docker 挂载数据卷的时候,产生出来的文件皆会是 root 权限,会出现无法写入的权限的问题
挂载时要注意
文件夹挂载
- 宿主机
文件夹不存在或空文件夹挂载进容器,容器中对应的文件夹将被清空 - 宿主机
非空文件夹挂载进容器,将会覆盖容器中原有文件夹
文件挂载
- 禁止将不存在的文件挂载进容器中已经存在的文件上
- 宿主机
存在的文件挂载进容器,将会覆盖容器中对应的文件,若文件不存在则新建
正文
这里通过遇到的问题来理解 Docker 容器用户 UID 的使用,以及了解容器内外 UID 的映射关系,教大家用三种方式来设定容器使用者权限,以解决上述遇到的问题
问题
如果在 Docker 启动时,没有指定用户运行,容器会默认以 ROOT 进行启动。如果在容器内对挂载目录下的文件进行了操作,则相应文件的所有者就会升级为root,但在容器外如果只有非root用户的权限,就无法对这些文件进行操作了
比如在容器中创建 111 文件
1 | pi@pi:~$ mkdir data |
看文件权限,可以看到文件是 root 权限
1 | pi@pi:~$ tree -pugf data |
而我们当前的用户是 pi 用户
1 | $ whoami |
当写入文件时,会因为权限问题被拒绝
1 | pi@pi:~$ echo 'ok' > data/111 |
情况2
再比如,容器里的 mysql 用户的 uid 是 2000,而宿主的 mysql 用户 uid 是 1000,即便宿主要挂载的目录权限是 mysql:mysql,容器里看到的权限也是 1000:1000 ,会因权限被拒绝
情况3
还有一种情况,宿主不存在被挂载的目录,
1 | pi@pi:~$ sudo docker run -it -v $PWD/data2:/test alpine /bin/sh -c 'touch /test/111' |
Docker会以root权限先创建该目录再挂载
1 | pi@pi:~$ ls -l |
这就导致以普通用户运行的容器进程无权限访问该目录
容器共享宿主机的UID
首先了解 UID、GID 的实现
Linux 内核负责管理 UID 和 GID,并通过内核级别的系统调用来决定是否通过请求的权限

比如,当一个进程尝试去写文件,内核会检查创建这个进程的的用户的 UID 和 GID,来决定这个进程是否有权限修改这个文件,这里不是使用 username(用户名),而是 UID
当 Docker 容器运行在宿主机上的时候,仍然只有一个内核,容器共享宿主机的内核,所以所有的 UID 和 GID 都受同一个内核来控制
为什么容器里的用户名和宿主不一样
比如,superset 容器的用户叫做 superset,而本机没有 superset 这个用户,这是因为 username 不是 Linux Kernel 的一部分
简单的来说,username 是对 uid 的一个映射,**权限控制的依据是 uid,而不是 username**,验证权限时只认 UID 和 GID,所以不管用户名是什么,对一个特定文件的所有者,容器内外都是只认相应的 UID 的
解决
使用 Docker 指定使用者
可以通过 -u 将使用者 uid 及群组 gid 传入容器内
1 | docker run -it -v $PWD/data:/test -u 当前uid:当前gid debian /bin/bash |
查看当前使用者 uid 和 `gid
1 | $ id |
为了更加方便,上述指令可以改成
1 | docker run -it -v $PWD/data:/test -u $(id -u):$(id -g) debian /bin/bash -c 'touch /test/222' |
查看权限
1 | $ ls -l data/ |
使用 Dockerfile 指定使用者
也可以在 Dockerfile 内直接指定使用者
1 | USER 1000:1000 |
但不推荐该方式,除非是在 容器 内独立建立使用者,并且指定权限
通过 docker-compose 指定使用者
通过 docker-compose 可以用 user 指定使用者权限来进行挂载
1 | services: |
接着可以通过 .env 文件来指定变量的值
1 | CURRENT_UID=1001:1001 |
通过用户空间
通过 user namespace 技术,把宿主机中的一个普通用户(只有普通权限的用户)映射到容器中的 root 用户
在容器中,该用户在自己的 user namespace 中认为自己就是 root,也具有 root 的各种权限,但是对于宿主机上的资源,它只有很有限的访问权限(普通用户)
步骤一
Ubuntu 不需要该步骤
Kernel 内核开启 namespace
1 | grubby --args="namespace.unpriv_enable=1 user_namespace.enable=1" --update-kernel="$(grubby --default-kernel)" |
需要重启系统生效
1 | reboot |
步骤二
新建用户,这里的用户名为 2bacc
1 | groupadd -g 500000 2bacc |
在 /etc/subuid 和 /etc/subgid 文件中都修改自动生成的从属ID范围
1 | echo '2bacc:500000:65536' >>/etc/subuid |
文件格式是
1 | USERNAME:UID:RANGE |
| 项 | 注释 |
|---|---|
| USERNAME | 映射到容器内的用户名 |
| UID | 为用户分配的初始 UID |
| RANGE | 为用户分配的 UID 范围 |
用户 2bacc 在当前的 user namespace 中具有 65536 个从属用户,用户 ID 为 500000-565535,在一个子 user namespace 中,这些从属用户被映射成 ID 为 0-65535 的用户。subgid 的含义和 subuid 相同
步骤三
修改 /etc/docker/daemon.json 配置,新增 "userns-remap": "default" 选项,改好如下
1 | $ cat /etc/docker/daemon.json |
区别
"userns-remap": "2bacc"指定映射用户,演示用"userns-remap": "default"自动配置映射,实际用
注意:修改此项配置需要慎重,如果是已经部署了一套docker环境,启用此选项后,会切换到隔离环境,以前的docker容器将无法使用!
然后重启服务
1 | systemctl restart docker |
接下来我们发现在 /var/lib/docker 目录下新建了一个目录 500000.500000 也就是我们之前指定的
1 | $ ls -l /var/lib/docker |
目录下的内容与 /var/lib/docker 基本一致,说明启用用户隔离后文件相关的内容都会放在新建的 500000.500000 目录下
1 | $ ls -l /var/lib/docker/500000.500000/ |
[
]
测试
在容器里面创建一个 UID 为 1000 的 test 用户,他能读取挂载在容器里面的什么权限的文件呢?
我们先将
在宿主系统的 /data/test 目录上,创建几个不同的测试文件
1 | mkdir -p /data/test && cd $_ |
一个属主和属组都为 501000 的文件,权限为600,文件名为:501000.txt
1 | echo '1111' > 501000.txt \ |
一个属主和属组都为 2bacc(UID为500000)的文件,权限为600,文件名为:2bacc-root.txt
1 | echo '2222' > 2bacc-root.txt \ |
一个在系统上权限为 root(UID为0)的文件,权限为600,文件名为:root.txt
1 | echo '3333' > root.txt \ |
一个在系统上权限为 root(UID为0)的文件,权限为666,文件名为:test.txt
1 | echo '4444' > test.txt \ |
查看下
1 | $ ls -l |
启动一个的容器,并在里面创建一个 UID 为 1000 的 test 用户,进入挂载的 /test 目录
1 | docker run --rm -it -v /data/test:/test centos bash |
先在容器内执行 sleep 1000,然后在宿主机查看下进程所有者,已经恢复为 root
1 | $ ps -elf | grep -v grep | grep sleep |
查看权限如下
1 | [test@d8d39b0128bc ~]$ id |
结果是只有 501000.txt 和 test.txt 这两个文件可以看到里面内容,剩下两个均没有权限
1 | [test@d8d39b0128bc test]$ cat 2bacc-root.txt |
其中 501000.txt 有权限,是因为在系统上这个文件的属主和属组都是 501000,其映射在容器里面的属主和属组就是 1000,所以 UID 为 1000 的 test 用户可以查看里面的内容
1 | $ ls -l 501000.txt |
而 test.txt 能查看,因为文件的权限是 666,other 位的权限是 rw 所以其他用户都有权限查看
1 | $ ls -l test.txt |
如果要给容器里面挂载一个有 root 权限的目录,在 宿主 上就需要给这个目录权限给 2bacc
比如要挂载的目录是 /data/test,那命令就是命令如下
1 | chown -R 2bacc:2bacc /data/test |
然后再容器里查看下,就都是 root 权限了
1 | [test@d8d39b0128bc test]$ ls -al |
临时禁用
一旦设置了 "userns-remap" 参数,所有的容器默认都会启用用户隔离的功能,有些情况下我们可能需要回到没有开启用户隔离的场景
可以通过 --userns=host 为单个的容器禁用用户隔离功能。或在 version: "3" 版本的 docker-compose.yml 文件中对应的容器增加 userns_mode: "host"
以同样的 2bacc 1000 为例子,启动的时候加上 --userns=host
1 | docker run --rm -it --userns=host -v /data/test:/test centos bash |
先在容器内执行 sleep 1000,然后在宿主机查看下进程所有者,已经恢复为 root
1 | $ ps -elf | grep -v grep | grep sleep |
要是有点晕就自己操作一遍,啥都会明白…