$ docker run --rm \
--user=$UID:$(id -g $USER) \
-v "$PWD":/project \
-v "$PWD/models":/project/models \
debian \
bash -c "touch /project/tmp.txt"
这样倒是不会报错了,但这样容器运行过程往 /project
写的临时文件都会出现在宿主机上。
可见,user
参数并不能解决所有问题。它存在两个问题:
(1)指定的用户不存在于容器内的 /etc/passwd
中,shell 无法显示用户名。
(2)user
参数会指定容器运行时刻的用户和主机一致,因此持有主机挂载的用户目录,但容器内非挂载的目录均无权限。
4. Docker 挂载绑定最佳实践——神秘的 docker-entrypoint.sh
我们需要一种手段,既可以像 user
参数一样在容器运行时可以将用户切换到和主机相同的用户,又希望 Docker 容器保留 root
用户,并给主机用户想要访问的目录授权(对特定目录 chown
、chmod
等)。(普通用户和 root 反复横跳😂)
Docker 官方文档对 Entrypoint
介绍时给出了一种最佳实践(官方认证的最佳实践)。
首先编写如下的 Dockerfile:
FROM debian
RUN apt-get update && apt-get -y --no-install-recommends install \
ca-certificates \
curl \
dirmngr \
RUN gpg --keyserver ha.pool.sks-keyservers.net --recv-keys B42F6819007F00F88E364FD4036A9C25BF357DD4
RUN curl -o /usr/local/bin/gosu -SL "https://github.com/tianon/gosu/releases/download/1.4/gosu-$(dpkg --print-architecture)" \
&& curl -o /usr/local/bin/gosu.asc -SL "https://github.com/tianon/gosu/releases/download/1.4/gosu-$(dpkg --print-architecture).asc" \
&& gpg --verify /usr/local/bin/gosu.asc \
&& rm /usr/local/bin/gosu.asc \
&& chmod +x /usr/local/bin/gosu
COPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
RUN chmod a+x /usr/local/bin/docker-entrypoint.sh
WORKDIR /project
ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]
该 Dockerfile 中安装了一个 gosu
的工具,并设置了程序的 Entrypoint
。由于 Docker 内使用 sudo
可能导致一些不可预知的 TTY 和信号转发问题,所以 Docker 官方推荐了使用 gosu
这个工具,用于保持容器在 root
用户下运行,并用 sudo
来切换到指定用户。
其中 docker-entrypoint.sh
内容如下:
#!/bin/bash
# 获取主机用户id
USER_ID=${LOCAL_USER_ID:-9001}
# 给主机用户授权制定的非绑定挂载目录
chown -R $USER_ID /project
# 创建和主机用户相同uid的用户,名为user
useradd --shell /bin/bash -u $USER_ID -o -c "" -m user
usermod -a -G root user
export HOME=/home/user
exec /usr/local/bin/gosu user "$@"
可以看到 docker-entrypoint.sh
中创建了一个名为 user
的用户,该用户的 uid 由 docker run
的参数传入,这里利用了 linux 系统的一个特点,容器内外用户权限的记录和用户的名字无关,只和 uid
有关,因此容器内我们将用户命名为 user
没有影响。docker-entrypoint.sh
最后一行调用 gosu 来切换到 user
用户并执行 Dockerfile 中的用户命令。
有了如上两个脚本,我们构建镜像并执行:
$ docker build -t test_volume .
运行容器时指定 LOCAL_USER_ID
参数:
$ docker run --rm \
-e LOCAL_USER_ID=$(id -u $USER) \
-v "$PWD/models":/project/models \
test_volume \
sh -c "touch tmp.txt && touch models/model.txt"
$ ls -l models/model.txt
-rw-r--r-- 1 current_user current_user 0 Sep 28 06:41 models/model.txt
可见不仅容器内往挂载目录 /project/models
写入的文件 model.txt
所有者是主机用户,而且在容器内往非挂载目录 /project/tmp.txt
写入文件也不会遇到权限问题。
5. 总结
Docker 运行时容器内默认使用 root 用户运行,但是我们不是总是想要用 root 用户,因为有时候我们希望容器计算产生一些文件,并通过 volume 的绑定挂载在主机上获取。特别是我们用 jenkins 等工具写一些持续集成的脚本时候。容器内用 root 用户运行会导致产生的文件也是 root 用户的,主机上没有读取权限。因此我们需要让容器在运行的时候切换到主机上的用户。
Docker 对于这种情况仍然没有提供足够便利的基础设施,我们采用了 Docker 官方目前推荐的一个方式,通过编写一个 docker-entrypoint.sh
脚本作为 Dockerfile 的 Entrypoint,脚本中创建和主机上相同 uid
的用户,并通过 gosu
工具切换到该用户执行命令。uid
需要在 docker run
阶段通过参数传入。我们在脚本中设置了缺省 uid ,上面的脚本随机选择了一个 9001,注意要将该缺省值避免设置成和 Docker 镜像中存在的用户冲突的 uid。
参考链接:
Deni Bertovic :: Handling Permissions with Docker Volumes
Best practices for writing Dockerfiles | Docker Documentation