我试图在Docker中挂载一个主机目录,但然后我不能从容器中访问它,即使访问权限看起来不错。

我正在做

sudo docker run -i -v /data1/Downloads:/Downloads ubuntu bash

然后

ls -al

它给我:

total 8892
drwxr-xr-x.  23 root root    4096 Jun 18 14:34 .
drwxr-xr-x.  23 root root    4096 Jun 18 14:34 ..
-rwxr-xr-x.   1 root root       0 Jun 18 14:34 .dockerenv
-rwx------.   1 root root 9014486 Jun 17 22:09 .dockerinit
drwxrwxr-x.  18 1000 1000   12288 Jun 16 11:40 Downloads
drwxr-xr-x.   2 root root    4096 Jan 29 18:10 bin
drwxr-xr-x.   2 root root    4096 Apr 19  2012 boot
drwxr-xr-x.   4 root root     340 Jun 18 14:34 dev
drwxr-xr-x.  56 root root    4096 Jun 18 14:34 etc
drwxr-xr-x.   2 root root    4096 Apr 19  2012 home

还有很多这样的台词(我认为这是相关的部分)。

如果我这样做

cd /Downloads
ls

结果是

ls: cannot open directory .: Permission denied

主机版本为Fedora 20, Docker 1.0.0, go1.2.2。

哪里出了问题?


当前回答

通常,主机卷挂载的权限问题是因为容器内的UID/GID不能根据主机上文件的UID/GID权限访问该文件。然而,这一具体情况有所不同。

权限字符串末尾的点,drwxr-xr-x。,表示配置了SELinux。在SELinux中使用主机挂载时,您需要将一个额外的选项传递到卷定义的末尾:

z选项表示绑定挂载内容在多个容器之间共享。 Z选项表示绑定挂载内容是私有的、非共享的。

你的卷挂载命令看起来像这样:

sudo docker run -i -v /data1/Downloads:/Downloads:z ubuntu bash

有关使用SELinux的主机挂载的更多信息,请参见配置SELinux标签。


对于以不同用户运行容器时遇到此问题的其他人,您需要确保容器内用户的UID/GID对主机上的文件具有权限。在生产服务器上,这通常是通过在映像构建过程中控制UID/GID来实现的,以匹配能够访问文件的主机上的UID/GID(或者更好的是,在生产中不使用主机挂载)。

命名卷通常优于主机挂载,因为它将从映像目录初始化卷目录,包括任何文件所有权和权限。这种情况发生在卷为空且容器是用命名卷创建的情况下。

macOS用户现在有OSXFS自动处理Mac主机和容器之间的UID/ gid。它不能帮助的一个地方是来自嵌入式虚拟机内部被挂载到容器中的文件,比如/var/lib/docker.sock。

对于每个开发人员都可能更改主机UID/GID的开发环境,我的首选解决方案是启动容器时使用一个以root身份运行的入口点,在容器中固定用户的UID/GID以匹配主机卷UID/GID,然后使用gosu从根删除到容器用户,以便在容器中运行应用程序。这个重要的脚本是我的基本映像脚本中的fix-perms,可以在:Docker base Images from Brandon Mitchell找到

fix-perms脚本的重要部分是:

# Update the UID
if [ -n "$opt_u" ]; then
  OLD_UID=$(getent passwd "${opt_u}" | cut -f3 -d:)
  NEW_UID=$(stat -c "%u" "$1")
  if [ "$OLD_UID" != "$NEW_UID" ]; then
    echo "Changing UID of $opt_u from $OLD_UID to $NEW_UID"
    usermod -u "$NEW_UID" -o "$opt_u"
    if [ -n "$opt_r" ]; then
      find / -xdev -user "$OLD_UID" -exec chown -h "$opt_u" {} \;
    fi
  fi
fi

这将获得容器内用户的UID和文件的UID,如果不匹配,则调用usermod来调整UID。最后,它进行递归查找以修复没有更改uid的任何文件。比起运行带有-u $(id -u):$(id -g)标志的容器,我更喜欢这样做,因为上面的入口点代码不需要每个开发人员运行脚本来启动容器,并且卷之外的任何由用户拥有的文件都将更正其权限。


您还可以让Docker通过使用一个执行绑定挂载的命名卷来从映像初始化一个主机目录。这个目录必须已经存在,并且您需要提供到主机目录的绝对路径,而不像组合文件中的主机卷可以是相对路径。该目录也必须为空,以便Docker对其进行初始化。定义命名卷到绑定挂载的三个不同选项如下:

  # create the volume in advance
  $ docker volume create --driver local \
      --opt type=none \
      --opt device=/home/user/test \
      --opt o=bind \
      test_vol

  # create on the fly with --mount
  $ docker run -it --rm \
    --mount type=volume,dst=/container/path,volume-driver=local,volume-opt=type=none,volume-opt=o=bind,volume-opt=device=/home/user/test \
    foo

  # inside a docker-compose file
  ...
  volumes:
    bind-test:
      driver: local
      driver_opts:
        type: none
        o: bind
        device: /home/user/test
  ...

最后,如果尝试使用用户名称空间,您将发现主机卷存在权限问题,因为容器的UID/ gid发生了移位。在这种情况下,可能最容易避免使用主机卷,而只使用命名卷。

其他回答

这个问题是因为在您的机器上启用了SELinux。检查下面并禁用它。

[root@nfs-server ~]# getenforce
Enforcing
[root@nfs-server ~]# setenforce 0
[root@nfs-server ~]# getenforce
Permissive

如果你只是想禁用SELinux,你可以使用——security-opt标签:disable标志来做到这一点。

docker run --security-opt label:disable -v /run/docker.sock:/run/docker.sock POWERFULLCONTAINER

通常,主机卷挂载的权限问题是因为容器内的UID/GID不能根据主机上文件的UID/GID权限访问该文件。然而,这一具体情况有所不同。

权限字符串末尾的点,drwxr-xr-x。,表示配置了SELinux。在SELinux中使用主机挂载时,您需要将一个额外的选项传递到卷定义的末尾:

z选项表示绑定挂载内容在多个容器之间共享。 Z选项表示绑定挂载内容是私有的、非共享的。

你的卷挂载命令看起来像这样:

sudo docker run -i -v /data1/Downloads:/Downloads:z ubuntu bash

有关使用SELinux的主机挂载的更多信息,请参见配置SELinux标签。


对于以不同用户运行容器时遇到此问题的其他人,您需要确保容器内用户的UID/GID对主机上的文件具有权限。在生产服务器上,这通常是通过在映像构建过程中控制UID/GID来实现的,以匹配能够访问文件的主机上的UID/GID(或者更好的是,在生产中不使用主机挂载)。

命名卷通常优于主机挂载,因为它将从映像目录初始化卷目录,包括任何文件所有权和权限。这种情况发生在卷为空且容器是用命名卷创建的情况下。

macOS用户现在有OSXFS自动处理Mac主机和容器之间的UID/ gid。它不能帮助的一个地方是来自嵌入式虚拟机内部被挂载到容器中的文件,比如/var/lib/docker.sock。

对于每个开发人员都可能更改主机UID/GID的开发环境,我的首选解决方案是启动容器时使用一个以root身份运行的入口点,在容器中固定用户的UID/GID以匹配主机卷UID/GID,然后使用gosu从根删除到容器用户,以便在容器中运行应用程序。这个重要的脚本是我的基本映像脚本中的fix-perms,可以在:Docker base Images from Brandon Mitchell找到

fix-perms脚本的重要部分是:

# Update the UID
if [ -n "$opt_u" ]; then
  OLD_UID=$(getent passwd "${opt_u}" | cut -f3 -d:)
  NEW_UID=$(stat -c "%u" "$1")
  if [ "$OLD_UID" != "$NEW_UID" ]; then
    echo "Changing UID of $opt_u from $OLD_UID to $NEW_UID"
    usermod -u "$NEW_UID" -o "$opt_u"
    if [ -n "$opt_r" ]; then
      find / -xdev -user "$OLD_UID" -exec chown -h "$opt_u" {} \;
    fi
  fi
fi

这将获得容器内用户的UID和文件的UID,如果不匹配,则调用usermod来调整UID。最后,它进行递归查找以修复没有更改uid的任何文件。比起运行带有-u $(id -u):$(id -g)标志的容器,我更喜欢这样做,因为上面的入口点代码不需要每个开发人员运行脚本来启动容器,并且卷之外的任何由用户拥有的文件都将更正其权限。


您还可以让Docker通过使用一个执行绑定挂载的命名卷来从映像初始化一个主机目录。这个目录必须已经存在,并且您需要提供到主机目录的绝对路径,而不像组合文件中的主机卷可以是相对路径。该目录也必须为空,以便Docker对其进行初始化。定义命名卷到绑定挂载的三个不同选项如下:

  # create the volume in advance
  $ docker volume create --driver local \
      --opt type=none \
      --opt device=/home/user/test \
      --opt o=bind \
      test_vol

  # create on the fly with --mount
  $ docker run -it --rm \
    --mount type=volume,dst=/container/path,volume-driver=local,volume-opt=type=none,volume-opt=o=bind,volume-opt=device=/home/user/test \
    foo

  # inside a docker-compose file
  ...
  volumes:
    bind-test:
      driver: local
      driver_opts:
        type: none
        o: bind
        device: /home/user/test
  ...

最后,如果尝试使用用户名称空间,您将发现主机卷存在权限问题,因为容器的UID/ gid发生了移位。在这种情况下,可能最容易避免使用主机卷,而只使用命名卷。

在我的情况下,问题就不同了。我不知道为什么,但是即使主机上的一个目录上运行了chmod 777,在Docker容器中它仍然是可见的755。

在容器内部运行sudo chmod 777 my_volume_dir修复了它。

警告:此解决方案存在安全风险。

尝试以特权模式运行容器:

sudo docker run --privileged=true -i -v /data1/Downloads:/Downloads ubuntu bash

另一种选择(我没有尝试过)是创建一个特权容器,然后在其中创建非特权容器。

从access.redhat.com: Sharing_Data_Across_Containers:

Host volume settings are not portable, since they are host-dependent and might not work on any other machine. For this reason, there is no Dockerfile equivalent for mounting host directories to the container. Also, be aware that the host system has no knowledge of container SELinux policy. Therefore, if SELinux policy is enforced, the mounted host directory is not writable to the container, regardless of the rw setting. Currently, you can work around this by assigning the proper SELinux policy type to the host directory": chcon -Rt svirt_sandbox_file_t host_dir Where host_dir is a path to the directory on host system that is mounted to the container.

这似乎只是一种变通办法,但我试过了,而且奏效了。