Docker: ротация логов

Продолжаю серию заметок про Docker. Я уже писал про контейнеризацию сервисов на примере MySQL, про data-only контейнеры и про service discovery с помощью dnsmasq. Сегодня пришла очередь поговорить про (я прошу прощения за аллегорию) продукты жизнедеятельности контейнера, а именно про логи.

Если не уделить должного внимания ротации логов, то можно попасть вот в такой просак.

Логи контейнеров

Docker захватывает всё, что выводится “на экран” внутри контейнера (STDOUT и STDERR), и записывает это в файл:

/var/lib/docker/containers/ID/ID-json.log

Посмотреть записанное можно командой:

docker logs ID

На сегодняшний день (начало 2015 года) нет встроенной возможности отключить логи или ограничить их размер, хотя сообщество разработчиков понимает, что в эту сторону необходимо копать: #6630, #7195, #8911.

Обновление: в Docker 1.6 добавили logging drivers, и стало возможно направить логи в syslog (--log-driver=syslog) или вообще отключить (--log-driver=none).

Тем не менее, с помощью стандартного инструмента logrotate можно уже сейчас настроить ротацию логов. И это совсем не сложно.

Создайте файл /etc/logrotate.d/docker-containers со следующим текстом внутри:

/var/lib/docker/containers/*/*.log {
    rotate 31
    daily
    nocompress
    missingok
    notifempty
    copytruncate
}

(по желанию можно заменить nocompress на compress и изменить число дней в опции rotate)

Параметр, который нельзя убирать — это copytruncate. Именно он делает возможной ротацию логов без их переоткрытия (надеюсь, это изменится в будущем, см. #7333).

Давайте попробуем в действии. Запустите временный контейнер:

docker run -i -t --rm ubuntu:14.04

И попросите его бесконечно печатать на экран:

while true; do echo 'Logging!'; sleep 1; done

В соседней консоли зафорсируйте ротацию:

logrotate -f /etc/logrotate.conf

И убедитесь, что она произошла, проверив содержимое папки /var/lib/docker/containers/ID.

Полдела сделано. Вторая проблема — это…

Логи процессов, запущенных внутри контейнера

Яркий пример активных “логописателей” — web-серверы Apache и nginx.

При установке этих продуктов в любом современном Linux-дистрибутиве автоматически активируется ротация логов (с помощью всё того же logrotate). При запуске же их внутри Docker-контейнера ротация не работает, потому что она завязана на cron, а cron сам собой не запускается.

Самое простое, что приходит в голову — это отключить логи вовсе. Как говорится, нет логов — нет вопросов, плюс уменьшите нагрузку на диск.

Если же логи хочется иметь, то есть как минимум два пути.

logrotate внутри контейнера (сложный способ)

Во-первых, папку с логами желательно вынести в отдельный том (про тома можно почитать здесь). Во-вторых, нужно обеспечить старт cron-а, который в свою очередь будет раз в день вызывать logrotate.

Другими словами, встает задача запуска нескольких процессов в одном контейнере.

В интернете можно встретить базовые Docker-образы, авторы которых заявляют, что мол у них всё схвачено — и cron, и logrotate, и запуск нескольких процессов. Типичный представитель — baseimage-docker. Я не рекомендую использовать этот образ. Ниже по тексту объясню почему.

Итак давайте сделаем тестовый образ с nginx внутри.

Dockerfile:

FROM ubuntu:14.04
RUN DEBIAN_FRONTEND=noninteractive apt-get install -y nginx
docker build --tag=rotation-test .

Запускаем:

docker run --name=rotation-test -d -p 80:80 rotation-test nginx -g 'daemon off;'

Вбейте локальный IP в браузер и убедитесь, что появилось “Welcome to nginx”.

Теперь давайте прибьем контейнер:

docker rm -f rotation-test

И добавим в образ cron и logrotate.

Официальная рекомендация для запуска нескольких процессов внутри контейнера — это Supervisor.

Создайте рядом с Dockerfile папку etc и положите внутрь следующие 3 файла:

crontab:

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

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

logrotate.conf:

daily
rotate 31
compress
missingok
notifempty
nocreate

/var/log/nginx/*.log {
    postrotate
        nginx -s reopen
    endscript
}

supervisord.conf:

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

[program:nginx]
command=nginx -g 'daemon off;'

[program:cron]
command=cron -f

А сам Dockerfile приведите к такому виду:

FROM ubuntu:14.04

RUN DEBIAN_FRONTEND=noninteractive apt-get update \
    && apt-get install -y nginx supervisor

ADD etc/* /etc/

Обновите образ и запустите контейнер:

docker build --tag=rotation-test .
docker run --name=rotation-test -d -p 80:80 rotation-test supervisord

Убедитесь, что внутри всё завелось:

docker top rotation-test

В правой колонке увидите:

CMD
/usr/bin/python /usr/bin/supervisord
cron -f
nginx: master process nginx -g daemon off;
nginx: worker process

Теперь наполните логи (понажимайте F5 в браузере), а затем зафорсируйте ротацию:

docker exec rotation-test logrotate -f /etc/logrotate.conf

И проверьте, что она состоялась:

docker exec rotation-test ls /var/log/nginx

access.log
access.log.1.gz
error.log

Ура! Контейнер защищен от переполнения и теперь уже не превратится в мусорный контейнер 😃

Обратите внимание! Мы поместили в образ вполне конкретные crontab и logrotate.conf. То есть попросили делать только то, что нам нужно. Универсальные базовые образы типа злосчастного baseimage-docker тоже запускают cron, но полагаются на стандартные системные варианты этих конфигов, а в них можно встретить и вызовы /etc/init.d, и другую не рассчитанную на работу в контейнере дичь.

Альтернативный (простой) подход

Этот хитрый способ я подсмотрел в официальном репозитории nginx. Он заключается в замене лог-файлов символическими ссылками на STDOUT и STDERR:

FROM ubuntu:14.04

RUN DEBIAN_FRONTEND=noninteractive apt-get install -y nginx \
    && ln -sf /dev/stdout /var/log/nginx/access.log \
    && ln -sf /dev/stderr /var/log/nginx/error.log

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

Как обычно, всё гениальное просто!