Докеризация PHP-приложения в единый Docker-образ Alpine+PHP+NGINX+logrotate

Идея

У меня есть несколько ультралегковесных PHP-скриптов, предоставляющих простой HTTP-API для тех или иных целей. Например, один из таких скриптов выполняет функции управления сетью (реализует протоколы SNMP, Telnet и т.д.) с доступом по HTTP-API. Но чтобы клиент мог эксплуатировать такой простой скрипт, ему необходимо установить и настроить WEB-сервер, РНР со всеми необходимыми расширениями, системные зависимости, такие как iproute2 и прочие и для многих клиентов это оказалось нетривиальной задачей. Так родилась идея поместить в один Docker-образ и скрипт и всё окружение, вместе со всеми необходимыми настройками. По идее, пользователь должен выполнить простую команду docker run ... чтобы всё это магическим образом сразу же заработало без какой-либо необходимости что-то устанавливать и настраивать самостоятельно.

Цель

  1. Нам нужен крайне легковесный Docker-образ, включающий в себя сам PHP-скрипт, php-fpm, nginx и остальное окружение, необходимое для работы.
  2. Доступ по HTTP должен быть только к единственному файлу index.php.
  3. Службы внутри контейнера должны работать от имени непривилегированного пользователя nobody, чтобы сделать контейнер чуточку безопасней.
  4. Так как клиенты разбросаны по всему миру, нужно чтобы часовой пояс клиента был корректным внутри контейнера, в том числе и для PHP.
  5. Все сообщения системных процессов (nginx, php и т.д.) должны выводиться на стандартный вывод stdout и stderr, как это требуется для процессов, работающих в контейнерах. Но выводить туда же логи самого PHP-скрипта может быть не совсем целесообразно, хотя и вполне допустимо. Всё же для логов PHP-скрипта условимся использовать отдельный лог-файл, который к тому же должен автоматически ротироваться без участия пользователя.

Вроде бы всё. Поехали.

Исходный код из данной статьи находится на GitHub: https://github.com/denisbondar/alpine-php-nginx-example

Перед прочтением статьи рекомендуется клонировать репозиторий или скачать архив с кодом — так будет проще и понятней, о чем идет речь далее.

Реализация

Следующее описание будет для нового проекта, который только создается, без привязки к моему конкретному примеру. Итак, начнем с самого минимума.

Структура файлов (контекст сборки)

.
├── app
│   └── public
│       └── index.php
├── Dockerfile
└── rootfs
    ├── etc
    │   ├── crontabs
    │   │   └── nobody
    │   ├── logrotate.conf
    │   ├── nginx
    │   │   └── nginx.conf
    │   ├── php7
    │   │   ├── conf.d
    │   │   │   └── custom.ini
    │   │   └── php-fpm.d
    │   │       └── www.conf
    │   └── supervisor
    │       └── conf.d
    │           └── supervisord.conf
    └── usr
        └── bin
            └── docker-entrypoint.sh

Показная здесь структура файлов подразумевает, что ваш PHP-проект на локальном компьютере находится в подкаталоге app текущего каталога (например, /home/coolhacker/my-cool-project/app). Если это не так — не страшно. Нужно будет чуть больше манипуляций при создании образа, но не существенно. Далее будет понятно, что и где будет отличаться.

В данном примере файлы PHP находятся в подкаталоге app проекта, в котором, в свою очередь, находится подкаталог public, содержащий файл index.php — к нему будет указан путь в конфигурации nginx.

Всё это дерево каталогов и файлов представляет собой контекст сборки Docker-образа.

Начинать будем с создания файла Dockerfile в корне каталога. В нашем примере это /home/coolhacker/my-cool-project/Dockerfile.

Dockerfile

Исходный образ

Для сборки собственного образа лучше всего брать исходный образ Alpine. Я пробовал построить свой образ на основе образа php-fpm-alpine, но в итоге остановился на использовании базового образа Alpine в качестве исходного. Первой строкой Dockerfile прописываем инструкцию:

FROM alpine:3.14

Это означает, что за исходный будет взят образ alpine версии 3.14.

Далее нужно установить весь необходимый софт.

Установка служб и системных зависимостей

Здесь остановимся подробней.

Во-первых, нужно точно понимать, что и зачем добавляется в образ. И, если можно избежать добавления лишних файлов — лучше так и поступить. К счастью, идея Alpine заключается как раз в том, что сама операционная система и дополнительные пакеты содержат минимум файлов, необходимый для их работы. Некоторые пакеты вообще могут состоять из одного единственного файла (как, например, php8). Поэтому важно понимать, что при установке php вы получите только PHP без расширений вообще. То есть только то, что фактически входит в ядро. Не больше.

Во-вторых, все пакеты легко можно отыскать на сайте https://pkgs.alpinelinux.org/ либо по названию пакета, либо по файлу, который необходим.

В-третьих, расширения PHP лучше устанавливать из пакетов, но можно также воспользоваться и docker-php-ext-install, однако, в таком случае придется устанавливать все зависимости вручную из пакетов.

Чтобы установить пакеты, в Alpine используется команда apk add после которой идут опции и список пакетов, разделенный пробелом. Пока что добавим в Dockerfile выполнение самой команды и укажем две опции --update чтобы обновить кэш репозитория и --no-cache чтобы не кэшировать индекс (он нам не понадобится внутри образа). Для выполнения команд используется инструкций RUN:

FROM alpine:3.14

RUN apk add --no-cache --update \

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

Далее будем добавлять строки с необходимыми пакетами.

Nginx

Установим пакет nginx. Просто добавим название пакета, чтобы получилось:

RUN apk add --no-cache --update \
    nginx \
PHP

В данном примере показана установка php7 (по факту будет установлена последняя версия 7.4) и некоторых базовых расширений. Добавляем их:

RUN apk add --no-cache --update \
    nginx \
    php7 \
    php7-fpm \
    php7-ctype \
    php7-mbstring \
    php7-json \
    php7-opcache \

Если вы хотите использовать php8 — не проблема. Только следует помнить, что пакет php8 включает один исполняемый файл с именем php8, а пакет php7 включает два исполняемых файла php и php7. Это может быть важно, если вы запускаете какие-то сценарии php из консоли.

Если вам нужны другие расширения — находите их здесь и добавляйте в список.

Необходимые системные утилиты

Далее добавим следующие системные утилиты, которые понадобятся для работы контейнера:

  • curl — для использования в директиве HEALTHCHECK в Dockerfile для проверки состояния контейнера. Можно не использовать эту директиву, но тогда есть шанс, что зависший php-fpm или nginx остановит работу вашего сервиса, в то время как контейнер будет считаться выполняемым (running);
  • tzdata — для обеспечения поддержки часовых поясов внутри контейнера;
  • tini — не обязательная, но очень желательная утилита — инициализатор процессов init, специально разработанный для запуска в контейнере. Позволяет корректно управлять процессами внутри контейнера и убивать зомби-процессы, не позволяя исчерпать пространство PID. Вместо использования этой утилиты можно запускать контейнер с параметром --init, однако добавление этой утилиты ни коем образом не вредит контейнеру в любом случае;
  • supervisor — так как идеология запуска процессов в линукс-контейнерах подразумевает запуск только одного процесса на контейнер, то в качестве этого одного процесса можно использовать супервизор, задача которого лежит в запуске остальных процессов;
  • logrotate — инструмент для ротации логов, если таковые есть. Вовсе не обязательно использовать ротацию логов внутри контейнера и можно сделать это внешним logrotate из операционной системы хоста. Либо же вообще не писать логи в файлы, а писать их на стандартный вывод. В общем, если необходимо иметь самодостаточный контейнер, который сам еще и логи приложения ротирует, то добавляем;
  • dcron — легковесный crontab. Необходим для периодического запуска процессов внутри контейнера. В данном примере он будет запускать logrotate для периодической ротации логов. Также может пригодиться для периодического запуска каки-то фоновых процессов вашего приложения. Если ни в чем из этого нет необходимости, можно не устанавливать;
  • libcap — инструмент, который используется для запуска cron от имени непривилегированного пользователя. Так как все процессы в целях безопасности в контейнере будут запускаться от имени пользователя nobody, то и cron должен запускаться от имени этого пользователя, что по умолчанию невозможно. Данный инструмент позволяет решить эту задачу. Если не устанавливали dcron, то в этом инструменте тоже нет необходимости.

Добавляем далее в инструкцию RUN эти инструменты:

RUN apk add --no-cache --update \
    nginx \
    php7 \
    php7-fpm \
    php7-ctype \
    php7-mbstring \
    php7-json \
    php7-opcache \
    curl \
    tzdata \
    tini \
    supervisor \
    logrotate \
    dcron \
    libcap \
Действия после установки

Сперва нужно разобраться с dcron. Нам нужно, чтобы он запускался от имени непривилегированного пользователя (что обычно невозможно). Для этого мы установили libcap. Назначим владельца файла crond нашего пользователя nobody и изменим setuid-бита на capabilities. Если dcron не устанавливали, то и строки эти добавлять не нужно. Если устанавливали, то добавляем:

    && chown nobody:nobody /usr/sbin/crond \
    && setcap cap_setgid=ep /usr/sbin/crond \

Теперь создадим все необходимые каталоги. Как минимум каталог /app для PHP-файлов. В нашем примере также существует каталог /logs для размещения файлов логов в нем. Добавляем в Dockerfile:

    && mkdir -p /app /logs \

Последим действием будет очистка от ненужных файлов. Нужно удалить все временные файлы, все логи, кэши, чтобы образ занимал как можно меньше места. Также удалить все базовые кронтабы (конфиги для cron) и удалить каталог с подключаемыми конфигами ротатора логов logratate — они нам точно не нужны, т.к. мы всё настроим в одном файле. Это ведь контейнер. Добавляем далее:

    && rm -rf /tmp/* \
    /var/{cache,log}/* \
    /etc/logrotate.d \
    /etc/crontabs/* \
    /etc/periodic/daily/logrotate

Как видно, последняя строка не содержит обратного слеша в конце — это означает конец строки.

Целиком весь файл Dockerfile на данный момент выглядит следующим образом:

FROM alpine:3.14

RUN apk add --no-cache --update \
    nginx \
    php7 \
    php7-fpm \
    php7-ctype \
    php7-mbstring \
    php7-json \
    php7-opcache \
    curl \
    tzdata \
    tini \
    supervisor \
    logrotate \
    dcron \
    libcap \
    && chown nobody:nobody /usr/sbin/crond \
    && setcap cap_setgid=ep /usr/sbin/crond \
    && mkdir -p /app /logs \
    && rm -rf /tmp/* \
    /var/{cache,log}/* \
    /etc/logrotate.d \
    /etc/crontabs/* \
    /etc/periodic/daily/logrotate

Такая длинная строка, состоящая из множества разных команд создает один слой в образе Docker. Чем меньше слоев — тем лучше.

Поехали дальше.

Копирование корневой файловой системы

Теперь нам нужно подготовить различные файлы конфигураций, которые будут скопированы в образ с сохранением иерархии. Создадим каталог rootfs там же, где находится файл Dockerfile и всю структуру каталогов внутри (подкаталоги etc, usr и все подкаталоги). Если вам не нужен cron, то не создавайте каталог crontabs и файл крона nobody внутри него; если не нужна ротация логов, не создавайте файл logrotate.conf. Всё остальное повторяйте в точности как есть.

Далее содержимое каталога rootfs нужно скопировать в корень Docker-образа. Добавляем в Dockerfile следующую инструкцию:

COPY rootfs /

Далее подробней о каждом файле из rootfs:

etc/crontabs

Внутри crontabs находятся файлы расписаний cron. Имя файла соответствует имени пользователю, от которого будут запущены процессы внутри этого файла расписания. Так как пользователь, от имени которого будут работать все процессы внутри контейнера — nobody, то и файл должен иметь такое же имя. Внутри файла одна единственная строка, запускающая службу logrotate ежедневно в 6:30 утра:

30 6 * * *    /usr/sbin/logrotate -v /etc/logrotate.conf

Если вам нужны какие-то еще задачи — добавляйте строки в этот файл, как обычно в crontab.

etc/nginx/nginx.conf

Внутри nginx находятся файлы конфигураций сервера Nginx. Мы изменим только главный файл nginx.conf, остальные оставим как есть. Прямо в главный файл впишем настройку server, так как внутри контейнера он все равно будет один — нет смысла загружать группу конфигураций, как это сделан по умолчанию.

Логирование в этом файле настраивается на стандартный вывод — /dev/stdout и /dev/stderr.

В nginx.conf внутри блока server настраивается единственный в контейнере сервер. Причем настраивается таким образом, что доступ разрешен только к файлу index.php (и к корню сервера, который считается тем же индексом). Вместо такой конфигурации можно использовать любую другую — как необходимо в вашем конкретном случае. Помимо этого настраивается location ~ ^/(fpm-status|fpm-ping), необходимый для контроля состояния пула php-fpm.

Подключения принимаются на порту 8080, так как nginx будет работать не от суперпользователя и в этом случае желательно использовать порты с номерами выше 1023 во избежание проблем с правами доступа.

etc/php7/conf.d/custom.ini

В файле custom.ini настраивается вся необходимая конфигурация php. Вывод ошибок выполняется в /dev/stderr Обратите внимание на настройку часового пояса — он читается из переменной окружения ${TZ}. К ней мы еще вернемся позже.

etc/php7/php-fpm.d/www.conf

Файл www.conf содержит настройки пула fpm. В качестве назначения для сообщений об ошибках также указан /dev/stderr. Кроме того, используется файловый сокет (который указан в nginx.conf), включаются служебные эндпоинты для контроля состояния fpm пула. Настраивается менеджер процессов на усредненные значения (так как мы не знаем, какие ресурсы будут доступны контейнеру у клиента).

etc/supervisor/conf.d/supervisord.conf

Так как в контейнере можно запустить только один основной процесс, а нам нужно целых три (nginx, php, cron), воспользуемся супервизором — он будет тем самым основным процессом, который породит остальные нужные процессы.

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

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

etc/logrotate.conf

Этот файл содержит минимальную базовую конфигурацию ротации лога, а также одну единственную настройку размещения логов с пустыми настройками (будут браться настройки по умолчанию). Если вам нужно задать какие-то иные параметры ротации — укажите их в базовых настройках. Этого будет достаточно. Если же вам нужно ротировать разные файлы журналов по-разному, то внутри фигурных скобок пропишите отличающиеся параметры — они перекроют параметры по умолчанию.

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

RUN chown -R nobody:nobody /app \
    && chown -R nobody:nobody /logs \
    && chown -R nobody:nobody /run \
    && chown -R nobody:nobody /var/lib \
    && chown -R nobody:nobody /var/log/nginx \
    && chown -R nobody:nobody /etc/crontabs 

Переключаемся на непривилегированного пользователя

Далее нужно сменить пользователя на nobody, так как все последующие действия должны быть выполнены от его имени. Добавляем в Dockerfile следующую инструкцию:

USER nobody

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

WORKDIR /app

Копирование файлов PHP-скрипта в Docker-образ

Так как все рабочие PHP-файлы находятся в подкаталоге app текущего каталога, то скопировать их в каталог /app образа можно за одно действие. Здесь важно то, что владелец файлов и подкаталогов должен быть nobody. Добавляем в Dockerfile следующую инструкцию:

COPY --chown=nobody:nobody app /app

Если у вас PHP-файлы расположены не в подкаталоге (как в данном примере в подкаталге app), то в простейшем случае придется перечислить все подкаталоги и файлы в инструкции COPY. Последний аргумент — путь назначения.

Объявление внешних томов и портов

Если приложение ведет файл журнала в каталоге /logs, как это было задано в изначальной задаче, то было бы правильным разместить этот каталог за пределами контейнера в файловой системе хоста, даже если к нему не будет осуществляться доступ напрямую с хоста.

Ну и единственный порт, который принимает подключения — это порт 8080. Так как мы запускаем nginx не от имени суперпользователя, то не желательно либо даже невозможно использовать порты с номерами ниже 1024 (они доступны только пользователю root). Поэтому используем привычный порт 8080. В любом случае при запуске контейнера можно опубликовать этот порт на любой другой порт хоста, например, на порт 80.

Добавляем в Dockerfile:

VOLUME "/logs"

EXPOSE 8080

Точка входа ENTRYPOINT

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

Обычно точкой входа является скрипт с именем наподобие entrypoint.sh, который инициализирует контейнер при старте. Мы будем в такой инициализации устанавливать переменную окружения ${TZ}, в которую запишем часовой пояс. Обычно, но не всегда, в самом конце скрипта entrypoint.sh присутствует команда exec которая запускает переданную в аргументах команду.

Наш скрипт находится в rootfs по пути usr/bin/docker-entrypoint.sh и будет скопирован вместе с остальными файлами на сервер. Этот скрипт обязательно должен быть исполняемым (то есть иметь установленный флаг x).

Для большинства случаев будет достаточно добавить в Dockerfile следующую команду:

ENTRYPOINT ["/usr/bin/docker-entrypoint.sh"]

Это значит, что если запустить контейнер, указав, например, команду pwd, то фактически при старте контейнера будет выполнена команда /usr/bin/docker-entrypoint.sh pwd.

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

ENTRYPOINT ["/sbin/tini", "--", "/usr/bin/docker-entrypoint.sh"]

Выполняемая по умолчанию команда

Теперь необходимо указать выполняемую по умолчанию команду. Это та основная команда, которая будет выполняться в контейнере по умолчанию, если при запуске не указать её явно в аргументах docker run.

Для нашего контейнера таким основным процессом будет супервизор. Именно он должен запуститься как главный процесс и затем он породит все необходимые процессы (nginx, php-fpm, crond), которые будут работать в foreground без демонизации.

Эту команду можно легко переопределить, передав другую команду в качестве главного аргумента команд docker run или docker exec.

Однако стоит понимать, что в любом случае команда будет передана в аргументы скрипта, указанного как ENTRYPOINT.

Добавим в Dockerfile команду по умолчанию:

CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"]

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

/sbin/tini -- /usr/bin/docker-entrypoint.sh /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf

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

Проверка состояния контейнера

Благодаря инструкции HEALTHCHECK можно запускать периодическую проверку жизнеспособности контейнера. В данном примере будем обращаться каждые 30 секунд (по умолчанию) к эндпоинту http://127.0.0.1:8080/fpm-ping. Если последние 3 проверки (по умолчанию) будут провалены, контейнер будет считаться нерабочим.

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

Добавим в Dockerfile инструкцию:

HEALTHCHECK --timeout=10s CMD curl --silent --fail http://127.0.0.1:8080/fpm-ping

На этом создание Dockerfile можно считать завершенным.

Сборка и запуск

Здесь будут рассмотрен простой способ создания образа, его распространения и запуска. Справку по всем командам и параметрам можно получить из официальной документации.

Сборка

При выполнении сборки в файловое пространство демона containerd копируются все файлы из контекста. В нашем случае контекст — это все каталоги и файлы, расположенные там же, где и файл Dockerfile, но если этот файл расположен в другом месте — это можно указать отдельно. В общем случае этот файл располагается там же, где и остальные файлы и здесь рассматривается именно такой вариант.

Если есть какие-либо файлы или каталоги, попадание которых в пространство сборки не имеет смысла, целесообразно добавить их имена в специальный файл .dockerignore, синтаксис которого целиком аналогичен синтаксису .gitignore. Обычно этот файл, как минимум, дублирует .gitignore, однако чаще содержит больше записей, ведь игнорируемых при сборке Docker-образа файлов может быть значительно больше, чем игнорируемых для репозитория.

Для построения образа на основании созданного Dockerfile необходимо запустить команду:

docker build --pull --tag=my-app-bundle-image .

Здесь:

  • --pull — загружать свежие версии Docker-образов, от которых зависит сборка.
  • --tag=my-app-bundle-image — финальному образу будет автоматически назначена данная метка. Если образ планируется разместить в Docker-хабе, метка должна состоять из имени пользователя (предприятия) Docker-хаба и через слеш имени образа. Также через двоеточие можно указывать версию. Если версию не указать, она автоматически устанавливается в latest. У одного и того же образа может быть сколько угодно меток. Чаще всего это используется для версионирования, когда новая версия получает новый номер, а также latest.
  • . — точка в конце строки указывает на путь к контексту. В данном случае это «текущий каталог». Так как не указан параметр --file, то Dockerfile будет использоваться из контекста.

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

Запуск

Для запуска контейнера из образа служит комнада docker run. В качестве аргументов этой команде в нашем случае необходимо обязательно передать каталог, который будет отображаться на каталог /logs внутри контейнера (для записи логов), а также опубликовать порт контейнера на порт хоста (по умолчанию в режиме моста).

Сперва необходимо создать каталог для логов. Создадим его прямо здесь, после чего добавим его в .gitignore и в .dockerignore:

mkdir -p log
chmod 777 log

Помимо этого мы передадим некоторые дополнительные аргументы, значение которых будет описано далее.

Для проверочного запуска контейнера из образа выполните команду:

docker run -it --name=my-app-container \
    -p 80:8080 \
    -v $(pwd)/log:/logs \
    -v /etc/localtime:/etc/localtime:ro \
    -v /etc/timezone:/etc/timezone:ro \
    my-app-bundle-image

Где:

  • -it — запуск в интерактивном режиме (подключение STDIN/STDOUT) и с созданием псевдо-TTY.
  • --name=my-app-container — контейнер после запуска будет иметь имя my-app-container, чтобы можно было обращаться к нему по этому заведомо известному имени. Если не задать имя, то оно будет выбрано случайным образом.
  • -p 80:8080 — публикация порта контейнера 8080 на 80й порт локального хоста.
  • -v $(pwd)/log:/logs — подключение локального тома log к каталогу /logs внутри контейнера.
  • -v /etc/localtime:/etc/localtime:ro и -v /etc/timezone:/etc/timezone:ro — транслировать внутрь контейнера файлы с часовым поясом и локальным временем. Это позволит получить часовой пояс внутри контейнера точно такой же, как и на хосте, а при старте контейнера скрипт точки входа дополнительно установит переменную окружения с часовым поясом, которую использует php.
  • my-app-bundle-image — имя образа (метка), на базе которого будет запущен контейнер

Запустится контейнер. В консоль начнут поступать сообщения о запуске процессов супервизором. Попробуйте открыть страницу в браузере http://127.0.0.1/ — вы увидите вывод phpinfo(). Также будет создан файл журнала, в который будет помещена одна строка. Проверьте это.

Чтобы контейнер работал в фоне, автоматически запускался при старте системы и перезапускался при падении, аргументы запуска следует немного изменить. Остановите выполнение текущего контейнера комбинацией клавиш Ctrl+C и выполните следующую команду:

docker run -d --name=my-app-container \
    --restart=always
    -p 80:8080 \
    -v $(pwd)/log:/logs \
    -v /etc/localtime:/etc/localtime:ro \
    -v /etc/timezone:/etc/timezone:ro \
    my-app-bundle-image

Здесь:

  • -d — запускать контейнер в фоне (поэтому ключи -it больше не нужны);
  • --restart=always — запускать контейнер при старте системы и всегда перезапускать при сбое (бесконечное число попыток). Вместо этого можно задать другие варианты.

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

Теперь, чтобы остановить контейнер, выполните команду:

docker stop my-app

Контейнер при этом не удаляется. Чтобы его снова запустить. выполните команду:

docker start my-app

Распространение образа

hub.docker.com

Самый простой способ распространения — загрузка в Docker-hub. Для этого нужно создать учетную запись на сайте https://hub.docker.com/ и присвоить метку образу в требуемом формате имя_пользователя/название_образа:версия. Версию можно не указывать — в этом случае будет автоматически использовано latest.

Чтобы назначить еще одну метку образу, выплоните команду:

docker tag my-app-bundle-image coolhacker/my-app-bundle-image

docker images | grep my-app-bundle

Теперь в спике образов видны два образа с одинаковым хэшем. На самом деле это один и тот же образ, но с двумя разными метками.

Тоже самое, если необходимо версионировать образы.

Для загрузки испльзуется команда:

docker push coolhacker/my-app-bundle-image

Если нужно загрузить все метки, которые присвоены указанному образу, можно просто воспользоваться аргументом --all.

tarball

Второй способ распространения — сохранение образа в tar. Это стандартный архив UNIX без сжатия. Чтобы сохранить образ в архив просто выполните команду:

docker save my-app-bundle-image -o my-app-bundle-image.tar

Вместо сохранения в файл можно вывести содержимое на стандартный вывод и перенаправить поток в gzip для сжатия.

Чтобы загрузить сохраненный таким способом образ на другой машине, скопируйте туда файл и выполните:

docker load -i my-app-bundle-image.tar

Вместо этого можно воспользоваться стандартный вводом, чтобы прочитать tar или сжатый архив.

Обновление образа на другой машине

Если вы распространяете образ с вашим приложением клиентам, но чтобы они имели возможность обновиться, нужно выполнить следующие действия:

docker stop my-app
docker rm my-app

dockrt pull coolhacker/my-app-bundle-image # если используется docker-hub
docker load -i my-app-bundle-image.tar # если используется распространение tarball

docker run -d --name=my-app-container \
    --restart=always
    -p 80:8080 \
    -v $(pwd)/log:/logs \
    -v /etc/localtime:/etc/localtime:ro \
    -v /etc/timezone:/etc/timezone:ro \
    my-app-bundle-image

Принцип теперь уже точно должен быть понятен.

Обслуживание

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

docker exec -it my-app-container sh

Вызов этой команды приведет к тому, что в работающем контейнере через точку входа будет выполнена команда sh а также будет подключен стандартный ввод/вывод и создан псевдо-TTY. В результате вы получите shell-доступ в контейнер, который выполняется. Можно посмотреть список процессов либо что угодно еще. Для отключения — Ctrl+C.

Уничтожить контейнер можно при помощи команды:

docker stop my-app-container
docker rm my-add-container

Уничтожить метку образа можно при помощи команды:

docker rmi my-app-bundle-image

Если у образа больше не осталось меток, то будет уничтожен и сам образ.

Спасибо за внимание.

Прокомментировать