О Dockerfile

2015-04-25

Кажется, я становлюсь фанатом Docker.

Фанаты калифорнийских Dockers

В прошлый раз мы развлекались с Докером, так сказать, вручную. Запускали контейнеры с башем, правили файлы конфигурации, сохраняли изменения в образы. Но оказывается, есть более милый сердцу админа способ создания образов. Это — Dockerfile.

Давайте так же создадим «эталонный» образ Nginxа на основе Ubuntu. Только сделаем это с помощью Dockerfile. Создадим каталог, допустим, ~/tmp/test-nginx. Создадим в этом каталоге файл Dockerfile, с большой буквы. И начнем записывать в этот Dockerfile инструкции по сборке образа.

Для начала сообщим, что основой нашего образа будет официальный образ Убунты.

FROM ubuntu:trusty

FROM означает из какого образа мы делаем свой. Докер самостоятельно выкачает этот базовый образ с Хаба.

Устанавливаем Nginx из штатных пакетов. При этом выставляем переменную окружения DEBIAN_FRONTENT в noninteractive, чтобы apt-get не спрашивал обо всяких возможных настройках и ставил все молча и по дефолту.

ENV DEBIAN_FRONTEND noninteractive
RUN apt-get update && apt-get install -y nginx

ENV выставляет переменную окружения. RUN запускает команду во временном контейнере.

Что нам еще нужно? Давайте полностью заместим конфигурацию Nginx чем-то своим. Создадим файл nginx.conf в том же каталоге, где находится наш Dockerfile, примерно такого содержания. (Помните, для продакшина вам нужен таки конфиг подлиннее.)

user www-data;
worker_processes 4;
daemon off;
pid /run/nginx.pid;

events {
    worker_connections 768;
}

http {

    include /etc/nginx/mime.types;
    default_type application/octet-stream;

    access_log /var/log/nginx/access.log;
    error_log /var/log/nginx/error.log;

    server {
        listen 80 default_server;
        root /www;
        index index.html index.htm;
    }

}

Здесь нам важен daemon off, чтобы Nginx не завершал свой стартовый процесс, чтобы он мог жить основным процессом контейнера. Слушаем порт 80. Корень сайта помещаем в каталог /www.

А теперь добавляем строчку в Dockerfile.

ADD nginx.conf /etc/nginx/

ADD добавляет файл в файловую систему временного контейнера. Можно даже указывать URL, и Докер скачает нужный файл при сборке образа.

Сообщаем Докеру, что в контейнере будет слушаться порт 80.

EXPOSE 80

Сообщаем Докеру, что каталог /www является томом и может содержать общие файлы, общие с хостом или другими контейнерами.

VOLUME /www

Ну и, наконец, сообщаем Докеру, что при запуске образа по умолчанию нужно запускать Nginx.

CMD /usr/sbin/nginx

Получился вот такой коротенький Dockerfile.

FROM ubuntu:trusty

ENV DEBIAN_FRONTEND noninteractive
RUN apt-get update && apt-get install -y nginx

ADD nginx.conf /etc/nginx/

EXPOSE 80

VOLUME /www

CMD /usr/sbin/nginx

Собираем вожделенный образ. Сразу дадим ему имя test-nginx с помощью ключа -t (tag). Последним параметром указываем каталог, где лежит Dockerfile и конфиг нашего Nginx.

$ docker build -t test-nginx ~/tmp/test-nginx
Sending build context to Docker daemon 3.584 kB
Sending build context to Docker daemon 
Step 0 : FROM ubuntu:trusty
 ---> d0955f21bf24
Step 1 : ENV DEBIAN_FRONTEND noninteractive
 ---> Using cache
 ---> 273e05ed8ff1
Step 2 : RUN apt-get update && apt-get install -y nginx
 ---> Running in b207412ba5bd
... тут долго выполняется apt-get
 ---> 9150ea91b621
Removing intermediate container b207412ba5bd
Step 3 : ADD nginx.conf /etc/nginx/
 ---> 8a8cddab26ce
Removing intermediate container ba0419146028
Step 4 : EXPOSE 80
 ---> Running in 7d586887a08b
 ---> c01822146625
Removing intermediate container 7d586887a08b
Step 5 : VOLUME /www
 ---> Running in cec6783ab269
 ---> eb2ebfbfe0d5
Removing intermediate container cec6783ab269
Step 6 : CMD /usr/sbin/nginx
 ---> Running in ce2b8d4c6fa7
 ---> d1a828f94444
Removing intermediate container ce2b8d4c6fa7
Successfully built d1a828f94444

Что произошло?

Ну, у нас появился образ.

$ docker images
REPOSITORY  TAG     IMAGE ID      CREATED        VIRTUAL SIZE
test-nginx  latest  b6f2c6be5516  2 minutes ago  227.6 MB

Который состоит из кучи слоев.

$ docker history test-nginx
IMAGE         CREATED        CREATED BY                                     SIZE
d1a828f94444  5 minutes ago  /bin/sh -c #(nop) CMD [/bin/sh -c /usr/sbin/n  0 B
eb2ebfbfe0d5  5 minutes ago  /bin/sh -c #(nop) VOLUME /www                  0 B
c01822146625  5 minutes ago  /bin/sh -c #(nop) EXPOSE map[80/tcp:{}]        0 B
8a8cddab26ce  5 minutes ago  /bin/sh -c #(nop) ADD file:70b50f5b68d7d9f286  353 B
9150ea91b621  5 minutes ago  /bin/sh -c apt-get update && apt-get install   39.28 MB
273e05ed8ff1  3 days ago     /bin/sh -c #(nop) ENV DEBIAN_FRONTEND=noninte  0 B
d0955f21bf24  5 weeks ago    /bin/sh -c #(nop) CMD [/bin/bash]              0 B
9fec74352904  5 weeks ago    /bin/sh -c sed -i 's/^#\s*\(deb.*universe\)$/  1.895 kB
a1a958a24818  5 weeks ago    /bin/sh -c echo '#!/bin/sh' > /usr/sbin/polic  194.5 kB
f3c84ac3a053  5 weeks ago    /bin/sh -c #(nop) ADD file:777fad733fc954c0c1  188.1 MB
511136ea3c5a  22 months ago                                                 0 B

Что ведь делает docker build? Для каждой команды в Dockerfile создается новый временный контейнер, в контейнере выполняются команды или меняются метаданные (см. строки, у которых SIZE = 0 B), измененный контейнер коммитится как еще один слой в создаваемый образ. Временный контейнер затем удаляется (Removing intermediate container). Последняя строчка в этой истории слоев — корень всех контейнеров. Последующие четыре строчки — это образ Убунту. А выше — наши команды.

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

К тому же, в данном случае есть потрясающий побочный эффект. Обратите внимание на эти строчки вывода команды docker build.

Step 1 : ENV DEBIAN_FRONTEND noninteractive
 ---> Using cache
 ---> 273e05ed8ff1

Временный контейнер не создавался. Слой был взят из кэша. Потому что позавчера я уже собирал контейнер на основе Убунту, где первой командой была установка переменной окружения DEBIAN_FRONTEND. Особенно заметным эффект кэширования будет, если вы поменяете конфиг Nginx. Предыдущая команда apt-get выполняться не будет. Результат установки пакетов Nginx будет взят из кэша. Что просто чудовищно сэкономит время. В результате небольшой тюнинг и пересборка образов — это быстро. Ну и отсюда эмпирическое правило: тяжелые и редко изменяющиеся команды сборки образа помещайте в начало Dockerfile.

Есть у сборки из Dockerfile и другое интересное свойство. Дело в том, что официальные базовые образы изменяются. В ту же Убунту приходят обновления безопасности и под именем ubuntu:trusty будет скрываться нечто совсем другое. Если вы нежно собирали образ ручками, то вам, как и в случае с виртуальными машинами, лучше сделать apt-get update && apt-get upgrade самостоятельно, накатив обновления дистрибутива еще одним слоем. В случае же Dockerfile вы просто заново запускаете docker build. Образ при этом пересоздается полностью. Но обновления базовой системы будут нижними слоями, что вроде как идеологически более правильно.

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

You're Master!

Ну давайте запустим наш Nginx. Команду, которая будет запускаться при старте контейнера, мы уже указали, так что достаточно только имени образа.

$ docker run -d --name test test-nginx

Ой. Мы забыли пробросить порт.

$ docker port test 80
2015/04/25 22:29:36 Error: No public port '80' published for test

Давайте попробуем ключ -P.

$ docker stop test
$ docker rm test
$ docker run -d --name test -P test-nginx

Ключ -P пробрасывает все задекларированные образом порты в некий дипазон портов-за-49000 на хосте. Это один из эффектов директивы EXPOSE в Dockerfile.

$ docker port test 80
0.0.0.0:49153

Ну, давайте зайдем на http://localhost:49153/. Действительно, Nginx. Только выдает 403 Forbidden. Потому что /www у него пустой.

Ну давайте вернемся к нашему прошлому режиму запуска образа. Явно прокинем порты ключом -p и подсунем свой index.html, который лежит в ~/tmp ключом -v.

$ docker stop test
$ docker rm test
$ docker run -d --name test -p 8080:80 -v ~/tmp/index.html:/www/index.html test-nginx

Теперь http://localhost:8080/ показывает то, что нужно.

Ну вроде и все.

Docker commands

Попробуйте еще поставить в nginx.conf вывод логов в stdout. Тогда логи Nginx, пока он вертится в контейнере, можно будет смотреть командой docker logs.

Забавно получается, вся эта демонизация, когда мы отключаемся от консоли, пишем в логи и т.п., совершенно не нужна, когда у нас контейнеризация.