В статье на Хабре обсуждался «docker way»(TM), который гласит: один контейнер — один процесс.
one process per container
Each container should have only one concern

Decoupling applications into multiple containers makes it much easier to scale horizontally and reuse containers. For instance, a web application stack might consist of three separate containers, each with its own unique image, to manage the web application, database, and an in-memory cache in a decoupled manner.

You may have heard that there should be “one process per container”. While this mantra has good intentions, it is not necessarily true that there should be only one operating system process per container. In addition to the fact that containers can now be spawned with an init process, some programs might spawn additional processes of their own accord. For instance, Celery can spawn multiple worker processes, or Apache might create a process per request. While “one process per container” is frequently a good rule of thumb, it is not a hard and fast rule. Use your best judgment to keep containers as clean and modular as possible.

If containers depend on each other, you can use Docker container networks to ensure that these containers can communicate.
Следование этому принципу при докеризации nginx чревато двумя последствиями. Настроить взаимодействие nginx и php-fpm в разных процессах через unix сокет немного сложнее, чем может показаться. И ротация логов, которая при обычной установке идет «из коробки», не может осуществляться в принципе, т.к. требует отправки сигнала USR1 nginx, для чего нужен ещё один процесс.

В результате обсуждения выяснилось, что можно вместо отправки сигнала USR1 nginx добавить опцию copytruncate в конфигурацию logrotate. Значит в контейнере нет необходимости в запуске нескольких процессов. Однако все действия по настроке запуска ротации логов по cron нужно все равно будет выполнить только не внутри контейнера, а на хосте где работет контейнер. При запуске в одном контейнере и веб-сервер, и ротации логов, отдельная настройка ротации на хосте уже не требуется.


В приведенных выше ссылках даны решения. Впрочем с первого раза все не заработало и пришлось искать причины. Поэтому кроме ссыок я привожу результаты своих опытов. Для того, чтобы можно было познакомиться со способом защиты от DDoS-атак вместо сервера nginx будет запускаться openresty (сборка nginx от Taobao со скриптовым движком Lua). Этот сервер имеет другое по сравнению с nginx расположение каталогов с файлами. Но все остальное абсолютно идентично.

Для начала создадим файл docker-compose.yml в корневом каталоге проекта:

version: "3"
services:
  app:
    build:
      context: ./docker/php
      # dockerfile: docker/php/Dockerfile
      args:
        UID: "3000"
    working_dir: /app
  nginx:
    build:
      context: ./docker/nginx
      # dockerfile: docker/nginx/Dockerfile
      args:
        UID: "3000"
    ports:
      - 8000:80

Мы предполагаем что сценарии создания контейнеров будут храниться в файлах docker/php/Dockerfile и docker/nginx/Dockerfile. Имя Dockerfile является именем по умолчанию, следовательно нет необходимости его явно задавать в конфигурации.

Создадим файл docker/php/Dockerfile:

FROM php:7-fpm
ARG UID
RUN addgroup --gid $UID --system app   && adduser --uid $UID --system --disabled-login --disabled-password --gid $UID app

Загружается образ php:7-fpm и создается пользователь с идентификатором заданным параметром UID (в docker-compose.yml UID: 3000) с именем app в группе app. Это нужно чтобы задать права на чтение сокета из контйнера, где будет запущен openresty.

Для того чтобы получить ротацию логов в nginx или openresty, необходимо чтобы контейнер не завершал работу при рестарте веб-сервера, а так же чтобы в этом же контейнере был запущен cron. То есть это не будет однопроцессный контейнер, но иначе ничего не получится. Запускать несколько процессов рекомендуется через supervisor.

Создадим файл docker/nginx/Dockerfile:

FROM openresty/openresty:xenial
RUN apt-get update && apt-get  install -y supervisor cron logrotate
COPY ./supervisord.conf /etc/supervisor/conf.d/supervisord.conf
COPY ./logrotate.conf /etc/logrotate.conf
COPY ./cron.d /etc/cron.d/nginx
ARG UID
RUN mkdir -p /var/log/supervisor   && chmod 644 /etc/logrotate.conf && chown root:root /etc/logrotate.conf   && chmod 644 /etc/cron.d/nginx && chown root:root /etc/cron.d/nginx   && addgroup --gid $UID --system app   && adduser --uid $UID --system --disabled-login --disabled-password --gid $UID app
ENTRYPOINT ["/usr/bin/supervisord"]

Сначала интсаллируются все необходмые программы. Затем копируются конфигурационные файлы из каталога ./docker/nginx/ во внутреннюю файловую систему контейнера. Далее некоторым из этих файлов присваиваются права 644 (в противном случае система не будет выполнять ротацию логов). И также создается пользователь и группа app с тем же самым идентификатором (UID: 3000).

Так же необходмио создать несколько конфигурационных файлов.

Файл docker/php/zz-docker.conf (имя zz-docker.conf присутсвует в конфигурации образа php:7-fpm. Это нигде не описано и может меняться. К сожалению в настоящий момент подробных описаний образов нет, и приходися их исследовать после загрузки из репозитария):

[global]
daemonize = no

[www]
;listen = [::]:9000 # Don't need this
listen = /sock/docker.sock
listen.owner = app
listen.group = app
listen.mode = 0660

Параметр listen = /sock/docker.sock будет тот же что и в конфигурации nginx.

Основной конфигурационный файл nginx придется переписать т.к. в openresty он не содержит необходимых параметров, а это расположение логов, иднтификатор процесса, пользователь (app), и каталог с конфигурациями виртуалных серверов (/conf.d).

user app;
error_log  /var/log/nginx/error.log debug;
pid /var/run/nginx.pid;
http {
    access_log  /var/log/nginx/access.log;
    include       mime.types;
    default_type  application/octet-stream;
    server {
        listen       80;
        server_name  localhost;
        location / {
            root   html;
        }
    }
    include /usr/local/openresty/nginx/conf/conf.d/*;
}

Создадим виртуальный сервер с конфигурацией server.conf:

server {
    listen 80;
    server_name local;
    root /usr/share/nginx/html;
    disable_symlinks off;
    client_max_body_size 50M;
    location ~ (/assets|/favicon.ico) {
        try_files /build$uri $uri =404;
    }
    location / {
      try_files $uri /app.php$is_args$args;
    }
    location ~ \.php$ {
      fastcgi_pass   unix:/sock/docker.sock;
        try_files      $fastcgi_script_name =500;
        fastcgi_split_path_info ^(.+\.php)(/.*)$;
        include        fastcgi_params;
        fastcgi_param  SCRIPT_FILENAME  $document_root$fastcgi_script_name;
    }
}

К прокси обращаемся не через порт, а через сокет unix:/sock/docker.sock.

Теперь создадим файл logrotate.conf:

/var/log/nginx/*.log {
        size=1k
        missingok
        rotate 8
        notifempty
        sharedscripts
        postrotate
                [ ! -f /var/run/nginx.pid ] || kill -USR1 `cat /var/run/nginx.pid`
        endscript
}


И задание для cron:

SHELL=/bin/sh
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
MAILTO=""

# m h dom mon dow user  command
*  *  *  *  *  root  logrotate -v /etc/logrotate.conf
#


Размер файла size=1k и зспуск ротации каждую минуту (* * * * *) не для рабочего сервера, а тоько для того чтобы можно было при имнимальных временных заратах наблюдать ротацию логов. Команда logrotate копирует логи в архивные файлы. Но пока не будет перезапущен nginx — не произойдет реального создания нового пустого файла логов. Для того чтобы nginx открыл логи заново служит устрашающая команда kill -USR1 `cat /var/run/nginx.pid`.

И наконец конфигурация supervisor:

[supervisord]
nodaemon=true
logfile=/dev/null

[program:nginx]
command=/usr/local/openresty/bin/openresty -g 'daemon off;'

[program:cron]
command=cron -f

Совершенно не имеет значения где все эти конфигурационные фйлы находятся, т.к. все пути задаются в операторах COPY из Dockerfile, и в значениях volumes из docker-compose.yml. Теперь нужно набраться терпения и записать все необходимые пути в docker-compose.yml.

version: "3"
services:
  app:
    build:
      context: ./docker/php
      args:
        UID: "3000"
    working_dir: /app
    volumes:
      - ./:/app
      - ./html:/usr/share/nginx/html
      - ./docker/php/zz-docker.conf:/usr/local/etc/php-fpm.d/zz-docker.conf
      - ./docker/sock:/sock
    expose:
      - 9000
    links:
      - mysql
  nginx:
    build:
      context: ./docker/nginx
      args:
        UID: "3000"
    ports:
      - 8000:80
    volumes:
      - ./:/app/
      - ./html/:/usr/share/nginx/html/
      - ./docker/nginx/nginx.conf:/usr/local/openresty/nginx/conf/nginx.conf
      - ./docker/nginx/conf.d/:/usr/local/openresty/nginx/conf/conf.d/
      - ./docker/nginx/log/:/var/log/nginx/
      - ./lua/:/usr/share/nginx/lua/
      - ./docker/sock/:/sock/
    links:
      - app
    depends_on:
      - app

Теперь можно добавить скрипты Lua (см. статью на Хабре).

apapacy@gmail.com
27 января 2018 года

Комментарии (25)


  1. nazarpc
    27.01.2018 06:15
    +2

    Зачем вам именно на сокетах? Разницу в скорости вы скорее всего не заметите, а проблем с портативностью и настройкой добавляется знатно.


    Кстати, я уже почти 3 года как сделал набор контейнеров, в котором кроме Nginx, PHP и ротации логов есть SSH, MariaDB и прочие плюшки (включая резервное копирование и восстановление): https://github.com/nazar-pc/docker-webserver
    docker-compose.yml там получается гораздо проще, хотя под капотом делает многое описанное в статье.


    1. apapacy Автор
      27.01.2018 12:25

      про ротацию логUSR1 nginx можно в двух словах кто отправляет вот этот самый kill USR1?


      1. nazarpc
        27.01.2018 13:05

        Никто не отправляет. Logrotate крутится в отдельном контейнере и просто обрезает старые файлы до 0 байт. Никакого перезапуска не требуется, вот используемый конфиг: https://github.com/nazar-pc/docker-webserver/blob/master/logrotate/logrotate.conf


        1. apapacy Автор
          27.01.2018 13:25

          Но пока процесс nginx не закроет эти файлы они не будут реально уменьшены для этого сигнал USR1 являптся обязательным


          1. nazarpc
            27.01.2018 13:40

            Думаю, вы ошибаетесь. truncate есть truncate, файл будет уменьшен сразу. На сколько я понимаю, USR1 нужен если вы файл перемещаете и создаете на старом месте новый пустой, в этом случае USR1 заставит Nginx открыть новый файл. В случае с truncate мы обрезаем исходный файл и переоткрывать ничего не нужно, Nginx без проблем дописывает новые записи в тот же файл. По крайней мере так оно у меня на сервере выглядит.


    1. lega
      27.01.2018 23:35

      А зачем вам ssh внутри контейнеров?


      1. nazarpc
        28.01.2018 00:07

        У меня новая версия сайта выкладывается посредством git push, для этого использую контейнер с SSH, там же стоит Composer. Зависит от workflow, контейнер совершенно опциональный, используйте если и когда нужно.


        1. lega
          29.01.2018 01:05

          В этом случае версию можно хранить на хосте и подключать как volume — не нужен ssh и перезаписей исходного образа будет меньше.


          1. nazarpc
            29.01.2018 01:12

            Я храню данные в подключаемом data-only контейнере, по сути, то же самое, но не привязано к локальной файловой системе. Можно volume вместо контейнера подключать. Никакой перезаписи исходного образа при этом нет.


  1. alekciy
    27.01.2018 08:39
    +1

    ротация лога не требует перезапуска nginx. Не сбивайте с толку людей.


    1. firk
      27.01.2018 10:21

      А ещё


      Этот сервер имеет другое по сравнению с nginx расположение каталогов с файлами. Но все остальное абсолютно идентично.

      Описание выглядит так, как будто это nginx с нескучными обоями. Впрочем я не в курсе, может быть так и есть. А может быть автор опять сбивает с толку людей.


      1. apapacy Автор
        27.01.2018 12:03

        если Вы про openresty то обояии является скриптовый движок Lua


        1. firk
          27.01.2018 15:35

          Обои тут это расположение файлов, которое не неотъемлемая часть программы, а все лишь вопрос того как вы её настроите. От вашей фразы создаётся впечатление, что сменить строчку с указанием расположения какого-нить access_log'а в конфиге для вас равноценно пересборке всего сервера, так же как для какого-то школьника было непосильной задачей залить новую картинку в список обоев.


    1. apapacy Автор
      27.01.2018 12:01

      исправил ошибку. спасибо за заиечание


  1. dmitriylyalyuev
    27.01.2018 10:16
    +1

    1. apapacy Автор
      27.01.2018 12:05

      исправил в тексте


  1. tzlom
    27.01.2018 13:20
    +2

    Надо вообще по другому делать.
    nginx запускается не как демон (daemon off; в конфиге или nginx -g «daemon off;»), логи выводятся в stdout и stderr соответственно и готово.
    Почему так правильно:
    0 — При краше докер контейнера логи не будут потеряны, см. п 4.
    1 — nginx написан грамотно и никаких зомби процессов оставлять он не будет, если вы из него что-нибудь запускаете, поэтому супервизор ему как таковой не нужен.
    2 — Перезапуски при крашах управляются через докер (или под чем у вас кластер), там есть соответствующие параметры.
    3 — Мониторинг и перезапуск правильно делать отдельным процессом, потому что рано или поздно захочется нотификаций, графиков нагрузки, вот это всё.
    4 — Логи управляются через докер, там есть пачка плагинов чтобы этим рулить как надо, заводить в системы мониторинга и агрегации логов, или на крайний случай делать таки логротейт но уже логов докера (не пиная нгинкс)


    1. apapacy Автор
      27.01.2018 13:49

      перенаправлять в стандартный вывод докера в статье на которую есть ссылка описан как один из вариантов. логи в моей конфигурации не теряются т.к вынесены в volume по поводу ротаций логов самого docker я ещё не разобрался но в репозитории docker есть многочисленные issue по ротации логов можно ли их делать без рестарта контейнера или сервиса docker?


      1. apapacy Автор
        27.01.2018 17:39

        Попробовал с плагинами логирования для докера. Простейши плагин для файловой системы (json) хранит все в папках с идентификатором контейнера и при пересборке контейнера удаляется. Более сложные плагины там вообще не подразумевают ротацию т.к. это уже не простое логирование в файл. Если нужно обычное файловом логирвании то как мне кажется лучше всего это делать все же через volume. Действительно для ротации нет необходимости посылать kill USR1 можно просто добавить copytruncate (хотя несколько записй лога при этому могут потеряться). Все же запуск в одном процессе может также иметь право на применение, т.к. в этом случае все настраивается совместно в одном контейнере. В то же время для действительно сложных систем с десятками и сотнями контейнеров централизованное управление логами лучше. То есть область применения этого решения малонагруженный веб-сервер когда запускается 2-3 контейнера и о них нужно забыть и в то же время иметь возможость посмотреть на логи.


  1. nkirnos
    27.01.2018 17:41

    Пост из разряда вредных советов. Из-за ротации логов, которые докер может может отправлять куда угодно, автор топика сделал очередной велосипед


    1. apapacy Автор
      27.01.2018 17:47

      Были рассмотрены два вопроса: ротация логов и взаимодействие контейнеров через unix- сокеты. Задача которую автор изначально поставил — получить ротацию файловых логов nginx. То что docker имеет возможность перенаправлять логи для обработки в (кстати не такое уж необозримое) количестио систем по обработке логов автор не подвергал сомнению.


      1. nkirnos
        27.01.2018 18:00
        +1

        Если потребовалась ротация логов сервиса, который крутится внутри контейнера, то это первый признак, что ты делаешь что-то не правильно. А добавив сюда еще взаимодействие через сокеты, я в этом убедился еще раз. В общем все проходят через то, что воспринимают контейнер докера как виртуальную машину, как только начнешь воспринимать контейнер как приложение, тогда все станет на свои места.
        Попробуйте масштабировать приложение, которое будет крутиться в контейнере, который создал автор топика и вы поймете что у вас увеличивается количество телодвижений. А докер это про то, чтобы это количество не увеличивалось


        1. apapacy Автор
          27.01.2018 18:16

          Да, это решение не для крупномасштабных проектов. Но обычных, рядовых не 10К проектов веб-сервер + база данных многократно больше, чем сложных и высоконагруженных.

          По сокетам vs 9000 порт вопрос неоднократно обсуждался и я даже не буду приводить за и против сокет vs 9000 порт. Я просто скажу что у администратора должна быть возможность выбора. Если 9000 порт работет «их коробки» во всех образах php-fpm (кстати, есть issue как раз по этому поводу на UNEXPOSE) то для работы через сокет нужны дополнительные действия. Это и было разобрано в статье. И не утверждается что сокеты это всегда лучше чем порты. Каждый выбирает то что ему подходит в кажом конкретном случае.


  1. OnYourLips
    27.01.2018 18:19
    +1

    Слишком грязно и сложно у вас получилось, явно не стоило статью про это писать. Так происходит из-за того, что вы нарушили основополагающее правило докера: не более одной однородной группы процессов на контейнер. В итоге вам пришлось делать эти уродливые костыли.

    Выносите работу с логами вне докера, а супервизор — на помойку. Управление процессами, логи, мониторинг, кроны и т.д. — всё это должно быть вне образов.
    Либо меняйте докер на LXD/OpenVZ.


    1. apapacy Автор
      27.01.2018 18:57

      В Аутентичном переводе звучит «В каждом контейнере должна решаться только одна проблема».
      Решение с супервизором, кстати это тоже из документации докера docs.docker.com/engine/admin/multi-service_container.
      Согласен что для масштабируемых решений логирование это отдельная проблема и должна решаться централизовано.