前言

最近发现 Docker 容器中的 PHP 项目在写日志的时候提示没有权限,看了一下项目目录下的 storage 目录的权限是这样的:

ll
total 8.0K
drwxr-xr-x 4 www-data www-data 4.0K Feb  7  2021 public
drwxr-xr-x 7 www-data www-data 4.0K Feb  7  2021 storage

排查

在 PHP-FPM 容器中使用的用户和组都是 www-data,这也就说明了为什么权限为 755 的时候无法正常写入。

然后我尝试在宿主机中创建 www-data 用户和组,并将项目下的 storage 目录的所有者和所有组都修改为 www-data。

那么问题来了,重启 PHP-FPM 发现依然无权限向 storage 目录中写入。容器内的用户和组和宿主机的都一样,这是为啥呢?

经过一番排查和面向搜索引擎编程,我发现问题主要可能是出在 UID 和 GID 上。

宿主机的 www-data UID 和 GID 分别是 1001 和 1002(因为 1001 的 GID 已经被占用了),而 PHP-FPM 容器内的 www-data UID 和 GID 都是 1000。

印证想法

为了印证上面的想法,我查阅了 Linux 相关资料。Linux 确实是使用 UID 来作为判断依据的,而不是用户名。

既然确定了问题所在,解决问题就非常简单了,在 PHP-FPM 容器内手动将 www-data 的用户和组的 ID 修改为与宿主机一致。

groupmod -o -g 1002 www-data
usermod -o -u 1001 -g www-data www-data

因为我宿主机的用户 www-data 的ID是 1001,www-data 组的 ID 是 1002.

通过上面的命令可以将容器内的用户和组 ID 修改为与系统一致,然后重启 PHP-FPM 容器,再次测试发现可以正常写入 storage 了。

疑问

虽然我们可以在 Dockerfile 和 Docker Compose 中配置使用什么用户和组来启动服务,但是这严重依赖于约定。比如我遇到的问题就是因为我宿主机系统中没有 www-data 这个用户和组。

就导致了前文所说的权限的问题,虽然 Docker Compose 可以指定 user 以及在 docker run 时 也可以指定,但是对于像 PHP-FPM 这类的服务,这个设置像意义不大,因为负责写入的是由 master 进程 fork 出来的 worker 进程。

而这些 worker 进程使用的用户和组是定义在 PHP-FPM 配置文件中的,默认是 www-data,UID 和组 ID 已经确认。

那么问题来了,我再 A Server 上定义的 UID 1000 和 GID 1000 在 B Server 上未必能用。我们只能在不同服务器上重新 build image,然后根据服务器环境 build 时将容器内的 UID 和 GID 修改成于系统对应。

我的方案

# Configure non-root user.
ARG PUID=1000
ENV PUID ${PUID}
ARG PGID=1000
ENV PGID ${PGID}

RUN groupmod -o -g ${PGID} www-data; \
    usermod -o -u ${PUID} -g www-data www-data

这样就可以保证宿主机的用户 ID 和容器中的一致,那么在 build image 之前,如果宿主机不存在用和组的话,还需使用如下命令手动创建:

sudo useradd \
    --user-group \
    --no-create-home \
    --shell /sbin/nologin \
    --comment "HTTP web service user" \
    www-data
sudo id www-data
(x)uid=1001(www-data) gid=1002(www-data) groups=1002(www-data)

将上面 sudo id www-data 这条命了输出的 UID 和 GID 写入 Docker Compose 的环境变量中,再 build 并启动容器。

对于不想重新构建镜像的同学,可以直接在容器中使用 groupmod 和 usermod 这两个命令来修改容器内用户名对应的 UID 和 GID 以保证于宿主机一致。

I hope this is helpful, Happy hacking…