1 апреля 2018 г.

Docker и permissions


В этой статье я расскажу, как я решил проблему с правами на файлы, которая проявляется при разработке веб-приложений с использованием Docker и контейнеров php-fpm, nginx, node.

Суть проблемы заключается в следующем. Предположим, что у вас выполнены следующие условия:
  • на вашей машине установлена ОС из семейства Linux, например, Ubuntu
  • вы логинитесь и работаете не root-пользователем, например, пользователь ubuntu с id = 1000
  • используете образы php-fpm, nginx, node из официального репозитория Docker Hub
  • исходный код вы редактируете не в докере, а на хост-машине, монтируя директорию с ним в докер-контейнеры
В такой ситуации получается, что владелец файлов исходного кода `id=1000`, а файлы, создаваемые приложением в процессе работы web-сервера (аплоад, кеши, логи и прочее) имеют владельца `id=33`, при этом права на запись у них есть только владельцу. Кроме того, файлы, создаваемые из консоли, например, файлы миграции, файлы кодогенерации, кеши, логи, ассеты, собираемые и публикуемые с помощью npm , будут созданы от имени root-пользователя, если не выполнять `docker run –user` с явным указанием пользователя. Даже если указать локального пользователя для выполнения команд от его имени, то могут возникнуть проблемы с правами записи кеша самих инструментов composer, npm.  Все вышеописанные файлы не получится ни редактировать, ни сменить права без root-пользователя. Это и есть проблема.

Решение

Суть решения в том, что мы меняем пользователя, под которым работает nginx, php-fpm, node на пользователя с тем же id, что и локальный пользователь, который редактирует файлы.

Вынесем id пользователя в build arguments для возможности указывать разных пользователей в разных окружениях. Команда Dockerfile `RUN` не поддерживает ENV-переменные, поэтому используем именно build arguments. Для development подойдет локальный пользователь, для stage и CI нужен тот пользователь, от имени которого запускаются build-скрипты в CI, например `gitlab-runner`, а на production лучше оставить www-data (id=33) и сделать выполнение консольных команд от его имени.

Привожу команды Dockerfile для каждого образа, которые создают нового пользователя внутри образа и прописывают его в конфиги приложений.
# PHP-FPM
ARG WEB_USER_ID=33
ARG WEB_USER_NAME=www-data
RUN echo "Building for web user: id=${WEB_USER_ID} name=${WEB_USER_NAME}"
RUN useradd -m -u ${WEB_USER_ID} ${WEB_USER_NAME} || echo "User exists"
RUN sed -i -- "s/user = www-data/user = ${WEB_USER_NAME}/g" /usr/local/etc/php-fpm.d/www.conf
USER ${WEB_USER_ID}

#NGINX
ARG WEB_USER_ID=33
ARG WEB_USER_NAME=www-data
RUN echo "Building for web user: id=${WEB_USER_ID} name=${WEB_USER_NAME}"
RUN useradd -m -u ${WEB_USER_ID} ${WEB_USER_NAME} || echo "User exists"
RUN sed -i -- "s/user nginx;/user ${WEB_USER_NAME};/" /etc/nginx/nginx.conf

#NODE
ARG WEB_USER_ID=33
ARG WEB_USER_NAME=www-data
RUN echo "Building for web user: id=${WEB_USER_ID} name=${WEB_USER_NAME}"
RUN useradd -m -u ${WEB_USER_ID} ${WEB_USER_NAME} || echo "User exists"
USER ${WEB_USER_ID}
Аргументы имеют значения по умолчанию, которые соответствуют production окружению. Мы создаем пользователя в системе, потому что конфигурация nginx и php-fpm требуют указания имени пользователя, а не uid. С помощью `sed` меняем конфигурацию приложения, указывая, что сервис должен работать от имени указанного пользователя. Последней командой мы указываем, что контейнер, созданный на основе этого образа, должен работать от имени указанного пользователя.

Сервисы php и nginx удобно поднимать с помощью `docker-compose`. Для указания пользователя в build arguments на разных окружениях можно использовать docker-compose.override.yml:
app:
    build:
        context: ./app
      args:
        WEB_USER_ID: 1000
        WEB_USER_NAME: roman
nginx:
    build:
        context: ./nginx
      args:
        WEB_USER_ID: 1000
        WEB_USER_NAME: roman
Контейнер с node, который используется с целью сборки через npm, не останется поднятым через docker-compose, поэтому отдельно соберем для него образ с аргументами ‘—build-arg’ и будем его запускать:
docker build -t project_node ./node --build-arg WEB_USER_ID="$WEB_USER_ID" --build-arg WEB_USER_NAME="$WEB_USER_NAME"
docker run --rm \
-v `pwd`:/var/www/project \
-w /var/www/project/app \
-e "HOME=/var/node" \
project_node bash
Здесь используются переменные bash-скрипта `$WEB_USER_ID` и `$WEB_USER_NAME`, определить которые можно несколькими способами. В разделе ниже о скрипте первой настройки есть пример получения значений этих переменных из ранее поднятого контейнера.
После сборки образов с указанием id и именем пользователя не требуется указывать дополнительно еще раз пользователя при использовании команд `docker-compose exec` и `docker run`, если только не нужно выполнить команду от имени root-пользователя.

Права на доступ к записи в некоторые каталоги

Некоторые программы внутри контейнера могут пытаться писать в директории, доступа к которым у них нет у нового пользователя. Это директории не внутри домашней директории нового пользователя. 
Если эта директория не монтируется с хост-системы, то права на запись можно дать отдельной командой в Dockerfile:
RUN mkdir -m 777 -p /var/node
или
RUN chown -R ${WEB_USER_ID} /var/node
Если же директория монтируется, то права на запись в нее будет только у root-пользователя. В этом случае изменять права нужно отдельной командой сразу после старта контейнера. Например, добавить в скрипт первой настройки:
docker-compose exec --user root app chown -R "$WEB_USER_ID" ./

Скрипт первой настройки

При первом развертывании проекта на машине после сборки образов и поднятии сервиса я предлагаю выполнять init-скрипт:
#!/bin/bash

echo "Permissions"
WEB_USER_ID="$(docker-compose exec -T app id -u | tr -d '[:space:]')"
WEB_USER_NAME="$(docker-compose exec -T app id -u -n | tr -d '[:space:]')"
if [ -z "${WEB_USER_ID}" ] || [ -z "${WEB_USER_NAME}" ]; then
  echo "Cannot get user id or name. Exit."
  exit 1
fi;
echo "Using WEB_USER_ID=$WEB_USER_ID, WEB_USER_NAME=$WEB_USER_NAME"
docker-compose exec -T --user root app chown -R "$WEB_USER_ID" ./

Скрипт получает из образа id пользователя, под которым работает сервис, затем устанавливает для всех файлов проекта владельца по этому uid.

При регулярном развертывании на production следует учесть, что при вышеуказанном подходе владельцем всех файлов проекта должен быть тот же пользователь, что указан в аргументах сборки, например, www-data (33). Здесь многое зависит о того, как организован deploy. Например, либо сразу запускать команды git от имени того же пользователя, либо потом менять владельца полученных новых файлов.

Заключение

Получаем, что web-сервер и команды из консоли выполняются от имени хост-пользователя, а значит, ни у какой из этих частей не возникнут проблемы с правами.

1 комментарий: