Docker: запуск нескольких приложений в одном контейнере

Прежде всего, несколько слов о том, правильно ли это.

В интернете можно встретить утверждения вида «философия Докера предполагает ровно один процесс». Я не думаю, что это правда. Философия Докера строится не вокруг технических деталей (таких как количество процессов), а вокруг идеи удобной упаковки, доставки и развертывания приложений. Вы сами выбираете подход — десяток изолированных микросервисов или один толстый контейнер all inclusive. Действуйте по ситуации!

Вот одна классическая ситуация: Apache + MySQL + cron.

Dockerfile может выглядеть так:

FROM ubuntu:14.04

RUN DEBIAN_FRONTEND=noninteractive apt-get install -y apache2 mysql-server

CMD (mysqld_safe&) && apache2ctl start && cron && sleep infinity

Идея простая: запускаем всё, что нужно, и бесконечно ждём. Это работает, но есть одна проблема — невозможно корректно остановить контейнер:

  • Команда docker stop отправит сигнал TERM, который никто не обработает.

  • Как следствие, мы ничего не сделаем, чтобы завершить рабочие процессы. Поэтому после тайм-аута Docker сделает это сам, послав им всем KILL.

Такой контейнер долго останавливается, и в логах Докера появляется «Container такой-то failed to exit within 10 seconds of SIGTERM — using the force».

Паузы можно избежать, используя вместо docker stop команды docker kill или docker stop --time=0.

Жёсткое «приземление» само по себе чревато тем, что будут прерваны операции ввода-вывода. То есть можно повредить файлы или потерять данные, которые не успели сброситься на диск.

Если контейнер не пишет ничего важного в файловую систему, то вполне допустимо его гасить и не переживать по этому поводу. Если же внутри работает важная база данных или происходит запись в тома (volumes), то лучше позаботиться о «graceful shutdown».

Для этого можно использовать менеджер процессов Supervisor. Неплохой инструмент, но он требует, чтобы дочерние процессы не «демонизировались» (то есть не уходили в фон). Из-за этого сервисы приходится запускать с флагами (порой экзотическими) или использовать костыли типа pidproxy.

Прежде чем выбирать Supervisor, обязательно изучите статью Subprocesses. Также имейте в виду, что он пишет собственные логи, которые желательно перенаправлять или отключать.

Альтернативный вариант, который выбрал для себя я — скрипт start.sh примерно такого вида:

#!/bin/bash

source start-utils

# Всё запускаем
mysqld_safe &
apache2ctl start
cron

# Ждём SIGTERM или SIGINT
wait_signal

# Запрашиваем остановку
pkill -x cron
apache2ctl stop
mysqladmin shutdown

# Ждём завершения процессов по их названию
wait_exit "apache2 mysqld_safe mysql cron"

Вспомогательные функции wait_signal и wait_exit вынесены в отдельный файл start-utils (который можно для удобства поместить в базовый образ):

stop_requested=false
trap "stop_requested=true" TERM INT

wait_signal() {
    while ! $stop_requested; do
        sleep 1
    done
}

wait_exit() {
    while pidof $1; do
        sleep 1
    done
}

Dockerfile:

FROM ubuntu:14.04

RUN DEBIAN_FRONTEND=noninteractive apt-get install -y apache2 mysql-server

ADD start-utils start.sh /
RUN chmod +x /start.sh
CMD [ "/start.sh" ]

Все материалы по теме Docker:

Видео:

Статьи:

Устаревшее:

Комментариев: 8

  1. Сергей:

    Добрый день. Спасибо за эту и другие статьи по Docker. Я правильно понимаю, что, даже в этом случае, если процессам в контейнере не хватило 10 секунд, они будут убиты «насильственным методом»? Есть ли возможность увеличить отведённое на «закругление» время?

  2. Алексей:

    @Сергей
    Используйте параметр -t:
    docker stop -t 15 имя-контейнера

  3. Сергей:

    Спасибо

  4. Иван:

    Не могли бы вы написать рабочий пример скрипта запуска php7-fpm + nginx? Взял ваш пример, сделал всё по анологии, но не могу избавиться от ошибки:

    panic: standard_init_linux.go:175: exec user process caused "no such file or directory" [recovered]
    goroutine 1 [running, locked to thread]:
    panic(0x88fc20, 0xc820121c70)
           /usr/local/go/src/runtime/panic.go:481 +0x3e6
    github.com/urfave/cli.HandleAction.func1(0xc8200ef2e8)
           /tmp/tmp.Hf57JpUD1p/src/github.com/opencontainers/runc/Godeps/_workspace/src/github.com/urfave/cli/app.go:478 +0x38e
  5. Сергей:

    Иван, если есть скрипты, убедитесь, что в начале прописан путь к интерпретатору (#!/bin/bash, например). Сам никак понять не мог, откуда такая ошибка, а оказалось такая ерунда.

    И права проверьте.

  6. Артём:

    Алексей, а вы не пробовали использовать в качестве основы для контейнеров phusion/baseimage? Они как раз тему корректного завершения процессов и наличия нескольких сервисов в одном контейнере поднимают и по их утверждениям сделали «самый правильный» образ для таких задач. Вот тут дают описание что, как и зачем: http://phusion.github.io/baseimage-docker/
    Я просто только познакомился с Docker (много понял по вашим записям, спасибо!) и интересно ваше мнение.

  7. Алексей:

    @Артём

    Моё мнение двухлетней давности — тут.

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

    С другой стороны, phusion/baseimage до сих пор жив и популярен. Значит «можно брать».

  8. Александр:

    Так ведь получается, что в данном примере контейнер по итогу получился «не самодостаточный». Подними его образ на другой системе, и придется скрипты за собой тащить.

Ваш комментарий