定制ENTRYPOINT自动修改Docker中volume的权限

volume的权限问题

Docker中,需要把host的目录挂载到container中作为volume使用时,往往会发生文件权限问题。 常见的现象是,container对该路径并无写权限,以致其中服务的各种千奇百怪的问题。

导致这类问题的原因,是container内外的UID不同。 比如,host当前使用docker的用户UID是1000(这是默认第一个用户的UID)。 如果container内的UID是2000,那么host创建的目录对container来说就并非owner,默认情况下不可写入。

此外还有一种情况,那就是挂载前,host上不存在被挂载的目录。 Docker会以root权限,先创建该目录,再挂载。 这就导致,即使host与container的UID都是1000,也会出现无写权限的情况。 这种现象,只会在初始化时出现,但也足够令新手困惑,令老手厌烦。

为什么在Dockerfile中不能把volume的权限配置好? 因为Dockerfile是对image的描述,而volume则是container的内容。 Dockerfile中做出的权限配置,对非volume来说是可以生效的,而对volume则不然。 本质上,host挂载到volume上的目录,是属于host的。 Dockerfile是在docker build期间执行,而volume则是在docker run的时候产生。

其实,Docker在自动创建volume路径时,应该再自动地把它修改为container内前台进程的user:group。 然而Docker目前并无此类机制,俺们这些用户就只能另谋出路。

一般的临时方案,都是去手动修改权限。 要么通过chown,把owner改成container内用户的UID; 要么通过chmod 777,搞成所有用户通用。 这些当然不是什么好的长期方案,也违背了Docker方便部署的初衷。

目前看来,最好的方案,还是定制Dockerfile的ENTRYPOINT

定制ENTRYPOINT

默认情况下ENTRYPOINT相当于是/bin/sh,只是准备简单的Shell运行环境。 定制ENTRYPOINT为一个复杂的脚本,就可以在每次container启动时,都执行一遍。 这个入口脚本,一般都是叫entrypoint.shdocker-entrypoint.sh,并无强制约定。

COPY entrypoint.sh ./

ENTRYPOINT ["./entrypoint.sh"]

在volume产生时,是docker run的准备阶段(create),而执行entrypoint.sh则是在启动阶段(start)。 所以,在entrypoint.sh中可以对volume做权限配置。 当然,权限配置需要root权限,entrypoint.sh需要在root用户下启动。

当然,也可以在一般用户下启动,需要chown时在提升至root权限。 不过这样会有安全隐患。 反之,以root执行entrypoint.sh,在其中以普通用户启动真正的服务,这样在服务被黑的情况下,能确保仅当前服务受影响。

选择ENTRYPOINT的shell

Docker基础镜像的发行版,通常是DebianAlpineDebian包含了BashDash,而Alpine则默认只有Ash

Ash的全称是Almquist shell,由Kenneth Almquist在1989年5月30日发布。 它是一种轻量级的Unix shell,在多个发行版上都有开枝散叶,详见ash variants。 现在,各大发行版上能看到的/bin/sh,基本都是Ash的一种。 Dash其实就是Debian上的一种Ash。

因此,用Ash来写ENTRYPOINT,会更具有通用性。 文件头可以使用以下形式的Shebang

#!/bin/sh

当然,什么也不写其实也默认是这个。 写上后显得更专业,对自己也算是一个提示吧,以免把Bash特有的一些用法,如sourceif [[ ... ]]等,用在脚本中。

样例

#!/bin/sh

mkdir -p "${DJANGO_DATABASE}" "${DJANGO_LOGS}" "${DJANGO_STATIC}"
python manage.py collectstatic --noinput
python -Wd manage.py migrate --fake-initial
chown -R "${DJANGO_USER}":999 "${DJANGO_SITE}"

if [ $# -gt 0 ]
then
    su "${DJANGO_USER}" -c "exec $*"
else
    su "${DJANGO_USER}" -c "exec uwsgi --ini uwsgi.ini --http=0.0.0.0:${DJANGO_PORT}"
fi

以上是一个Django网站镜像的entrypoint.sh。 其中,和本文关系密切的是chown那一行。 把相关volume的UID设为${DJANGO_USER},是镜像内的读写需要; 把GID设为999,是因为在宿主机上,Docker安装后,默认的docker用户组的GID就是999,便于宿主机进行操作。 最后的if代码块,用${DJANGO_USER}执行前台进程,提高了安全性(相当于Dockerfile里使用USER); exec相关的配置,给了一个CMD为空时的默认命令。

其它关于collectstaticmigrate等操作,是Django网站相关的有用配置,不在本文详解。

另一个复杂的参考,是postgres的官方镜像docker-entrypoint.sh。 其中做了很多事,但比较核心的,还是chownchmod那几行。

参考

还有一种方案,利用--volumes-from,可以指定一个Docker管控的非挂载数据卷。 这种数据卷没有权限问题,但宿主机的访问会有些麻烦。


相关笔记