Докер - путь программиста

Введение

В этой статье я хочу немного рассказать про Docker. Здесь я покажу, как начать с ним работать, самые основы. Большая проблема, с которой я сам столкнулся, когда изучал эту технологию, была в огромном количестве избыточной информации. Когда же я разобрался, всё оказалось намного проще.

Эту статью я писал, используя Debian 9 на VirtualBox. Конечно, это не обязательное условие, но докер сам по себе, на мой взгляд, ставит много лишнего в систему, поэтому я предпочитаю держать его в виртуальной машине.

Из чего состоит Docker

Сам по себе докер, это виртуальная машина. И нужно рассматривать его именно как полноценную виртуальную машину, в которой есть свой жёсткий диск, оперативная память и сетевая карта. Если вы работали с виртуальными машинами, напримкер, VirtualBox, то многие вещи будут уже знакомы, правда, с той разницей, что докер не имеет красивую GUI-оболочку, а управляется в основном из консоли.

Основные компоненты докера:

Нужно помнить, что в контейнере всегда запускается образ. При этом, если докер не нашёл образ в локальном хранилище, то он будет искать его в удалённом репозитории (как правило, на Dockerhub ).

Как запустить образ

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

docker run hello-world

Это приведёт к следующим действиям:

  1. Докер скачает образ hello-world в локальное хранилище.
  2. Создаст новый контейнер.
  3. Запустит контейнер, в котором выполнится команда /hello .

Если запустить команду несколько раз, то будет создано несколько контейнеров. Убедиться в этом можно, если выполнить docker ps -a :

% docker ps -a
CONTAINER ID        IMAGE               COMMAND             CREATED              STATUS                          PORTS               NAMES
9bc9586b6531        hello-world         "/hello"            9 seconds ago        Exited (0) 9 seconds ago                            upbeat_ellis
3db4dc859aaf        hello-world         "/hello"            11 seconds ago       Exited (0) 10 seconds ago                           suspicious_chebyshev
db61a2a26199        hello-world         "/hello"            About a minute ago   Exited (0) About a minute ago                       recursing_babbage

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

docker container prune -f
docker image prune -fa

Поздравляю! Теперь вы стали почти докер-мастером.

Немного теории

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

Именно на идеи изоляции процесса построена основная часть логики докера. Контейнер работает, пока работает начальный процесс (его называют entry point или command ). Как только процесс завершается, контейнер останавливается. Это ключевое отличие докера от привычных виртуальных машин (вроде VirtualBox).

Благодаря изоляции процессов, на одной машине можно запускать много версий приложения, которое в обычных условиях будет конфликтовать. Например, можно запустить сразу MySQL 8 и MySQL 5, nodejs 8 и nodejs 10. Без докера сделать это тоже можно, но проблематичней.

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

Как использовать ресурсы контейнера

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

Проброс портов

Для примера запустим контейнер с Debian 9 и пробросим локальный порт 3132 на 80 порт контейнера:

% docker run --tty --detach --entrypoint /bin/cat --name my_container --publish=3132:80 debian:stretch-slim
1b1d50ead8fff5eecd24029f9f70ae4ad40838d7fe6675e54ba0cd0fe93c83af
% docker ps 
CONTAINER ID        IMAGE                 COMMAND             CREATED             STATUS              PORTS                  NAMES
1b1d50ead8ff        debian:stretch-slim   "/bin/cat"          5 seconds ago       Up 5 seconds        0.0.0.0:3132->80/tcp   my_container

Пояснения к команде:

Общий принцип запуска контейнеров довольно простой:

docker run [аргументы и параметры] название_образа[:тег]

Аргументы, параметры и тег необязательны, их можно опускать. Но нужно помнить, что без аргументов образ сам по себе не пробрасывает порты и папки. Это всегда делается через аргументы при создании контейнера.

Теперь давайте рассмотрим, как выполнять команды внутри контейнера. Для примера установим и запустим консольный сервер php 7:

# Обновляем дерево пакетов
docker exec --tty --interactive my_container apt update

# Устанавливаем консольный php
docker exec --tty --interactive my_container apt install php7.0-cli

# Создаём index.php в контейнере
echo '<?php echo PHP_VERSION, PHP_EOL;' | docker exec --interactive my_container tee index.php

# Запускаем php-сервер на 80 порту контейнера
docker exec --tty --interactive my_container php -S 0.0.0.0:80 index.php

Пояснения к команде:
docker exec - выполняет команду внутри запущенного контейнера.
--tty - подключает виртуальную консоль. Без этого аргумента вывод будет неправильным.
--interactive - подключает ввод. Без него не будет работать клавиатура.
my_container - имя контейнера, в котором выполняется команда.
* команда и аргументы - команда, которая будет выполнена внутри контейнера.

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

% curl -s https://127.0.0.1:3132 
7.0.33-0+deb9u3

В этом примере я использовал две разные консоли. На одной я запускал сервер, а на другой curl. Ещё можно использовать браузер, если докер установлен у вас в системе.

Проброс папки

В предыдущем примере я создал index.php прямо в контейнере. Это не самый удобный способ разработки проектов в через докер. Во-первых, много файлов так не создашь, во-вторых ими сложно управлять, а, в-третьих, если удалить контейнер, они тоже удалятся. Чтобы решить эти проблемы, можно пробросить ( примонтировать ) папку из реальной машины в виртуальный контейнер. Делается это, как всегда, при создании контейнера, через аргумент --volume исходная_папка:папка_контейнера .

Прежде, чем начать что-то менять, надо удалить старый контейнер:

docker container rm -f my_container

Теперь подготовим наш “проект”:

mkdir ${HOME}/project
echo '<?php echo "PHP ", PHP_VERSION, " is the best!\n";' > ${HOME}/project/index.php

А теперь запускаем контейнер с пробросом папки проекта:

docker run --tty --detach --entrypoint /bin/cat --name my_container --volume ${HOME}/project:/project --publish=3132:80 debian:stretch-slim

Если вы помните, я удалил контейнер, в котором был установлен php, а это значит, что мне заново придётся установить пакет php7.0-cli:

# Устанавливаем php
docker exec --tty --interactive my_container apt update
docker exec --tty --interactive my_container apt install php7.0-cli

Теперь в контейнере есть и проект, и php, можно запускать сервер:

# Запускаем php-сервер на 80 порту контейнера
docker exec --tty --interactive my_container php -S 0.0.0.0:80 -t /project/

Теперь проверяем, как работает наш проект:

% curl -s https://127.0.0.1:3132 
PHP 7.0.33-0+deb9u3 is the best!

Для наглядности давайте создадим ещё один файл в “проекте”, чтобы удостовериться, что всё работает как надо:

# Добавляем файл
echo '<?php print_r($_SERVER);' > ${HOME}/project/test.php

# Проверяем
curl -s https://127.0.0.1:3132/test.php

Должно вывести что-то вроде этого:

Array
(
    [DOCUMENT_ROOT] => /project
    [REMOTE_ADDR] => 172.17.0.1
    [REMOTE_PORT] => 38694
    [SERVER_SOFTWARE] => PHP 7.0.33-0+deb9u3 Development Server
    [SERVER_PROTOCOL] => HTTP/1.1
    [SERVER_NAME] => 0.0.0.0
    [SERVER_PORT] => 80
    [REQUEST_URI] => /test.php
    [REQUEST_METHOD] => GET
    [SCRIPT_NAME] => /test.php
    [SCRIPT_FILENAME] => /project/test.php
    [PHP_SELF] => /test.php
    [HTTP_HOST] => 127.0.0.1:3132
    [HTTP_USER_AGENT] => curl/7.52.1
    [HTTP_ACCEPT] => */*
    [REQUEST_TIME_FLOAT] => 1557839721.1989
    [REQUEST_TIME] => 1557839721
)

Если у вас получилось, смело пишите в резюме, что владеете докером!

Собираем образ (image)

В предыдущих примерах я два раза установил один и тот же пакет в два разных контейнера. Это, мягко говоря, неудобно. Докер предлагает более гибкое и простое решения - собрать образ с уже установленными пакетами и некоторыми настройками.

Работа с образом не представляет ничего сложного. Нужно создать файл Dockerfile , указать в нём образ-источник, добавить свои команды и собрать.

Прежде, чем писать файл, я бы хотел определить проблемы, которые нужно решить:

  1. Установить пакет php7.0-cli.
  2. Сделать точку входа (чтобы не мучить cat)

Указать проброс портов или папки в Dockerfile файле нельзя (несмотря на директивы EXPOSE и VOLUME, проброс и монтирование можно делать только при запуске контейнера). Сделано это специально по причинам безопасности, иначе владелец образа в репозитории мог бы подключать любую системную папку в контейнер без ведома пользователя и красть данные.

Создаём Dockerfile в любой папке со следующим содержимым:

# Указываем образ-источник
FROM debian:stretch-slim

# Устанавливаем php + очистка системы
RUN apt-get update; apt-get install -y php7.0-cli; \
    apt-get clean && rm -rf /tmp/* /var/lib/apt/lists/*

# Указываем точкой входа консольный сервер php
ENTRYPOINT ["php", "-S", "0.0.0.0:80", "-t", "/project"]

Несколько слов об установке пакетов. Изначально в сборках *-slim отсутствует дерево пакетов (для уменьшения размера образа), поэтому надо обязательно выполнять apt-get update . После установки желательно удалить ненужные файлы и папки, поэтому выполняются команды очистки.

Небольшое отступление про точку входа. Как я уже говорил, основная специализация докера, это изолирование процесса. Поэтому в одном контейнере запускается всегда один процесс, и точка входа тоже всегда одна. Если нужно запустить вместе несколько процессов (например, php-frm и nginx), то пишется скрипт, который становится точкой входа и запускает нужные приложения. Но объявить несколько точек входа, на данный момент, нельзя.

Теперь нужно собрать образ командой:

docker build -t my_image:1.0.1 -f Dockerfile ${HOME}/project

Пояснения к команде:

Готово! Теперь можно запустить контейнер из нашего образа:

# Сначала удаляем старый контейнер
docker rm -f my_container

# Запускаем новый контейнер
docker run --detach --name my_container --volume ${HOME}/project:/project --publish=3132:80 my_image:1.0.1

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

% docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED              STATUS              PORTS                  NAMES
07c72af15d34        my_image:1.0.1      "php -S 0.0.0.0:80 -…"   About a minute ago   Up 59 seconds       0.0.0.0:3132->80/tcp   my_container

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

Что дальше

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

Попробуйте выполнить следующие шаги:

Шпаргалка по командам докера

# Запустить контейнер с автоматическим удалением (после остановки)
docker run --rm -ti my_container my_image

# Войти в командную оболочку в контейнере
docker exec -ti my_container sh

# Показать список всех контейнеров
docker ps -a

# Показать список образов
docker images

# Удалить нерабочие контейнеры
docker container prune -f

# Удалить лишние образы
docker image prune -fa

# Показать логи контейнера
docker logs my_container -f

Хорошая статья, мне понравилась. Оставлю отзыв!