☰ Оглавление

Как не бояться docker

Мне приходилось сталкиваться с docker, но каждый раз надо было быстро что-то сделать, вдаваться в детали было некогда, а под рукой были примеры, или быстро найденные в сети советы. Результат достигался очень быстро, но понимания не прибавлялось. Со временем, docker-а в моей жизни стало всё больше, и я решил разобраться.

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

Установка docker

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

Что нужно проверить перед началом работы: что у вас запущен демон. Если вы видите что-то такое:

$ docker info
Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?

значит надо запустить демона.

$ sudo systemctl start docker

В вашей системе команда может быть другой. Если же вы используете systemctl, возможно, вы ещё захотите включить демона в автозапуск systemctl enable.

Когда вы запустите демона, возможно появление такой ошибки

$ docker info
Got permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock: connect: permission denied

Обычно, достаточно или включить пользователя в группу docker (но я не рекомендовал бы так делать потому, что тем самым, вы фактически дадите пользователю рутовые права), либо просто использовать sudo.

$ sudo docker info
Containers: 0
....

Если вы это видите — ваш docker готов к использованию.

Как обычно используется docker и что будем делать мы

Обычно, в docker-конейнерах запускаются демоны/сервера/сервисы… Обычно, речь не идёт о каких-то интерактивных программах. Но сейчас мы хотим разобраться, поэтому будем делать не совсем стандартные вещи. В конце мы придём к классическому использованию docker, но уже с полным пониманием происходящего.

Два слова о том, как работает docker

Не вдаваясь в историю и в детали реализации на разных операционных системах, скажу, что сейчас в Linux docker использует не виртуализацию (как многие думают), а средства ядра, позволяющие создавать изолированные группы процессов. Т.е. запуская "виртуальную машину" (пишу это в кавычках), docker делает всего несколько системных вызовов и ядро создаёт для нового процесса отдельное пространство PID-ов, отдельную виртуальную сеть, отдельный набор ограничений по ресурсам. Процесс "запущенный в docker", на самом деле находится не в какой-то виртуальной машине (нет никакого эмулятора настоящей машины, никакой виртуальной сущности), он запущен на той же машине, тем же ядром, просто ядро ассоциирует его со специфическим набором настроек. Это почти то же самое, что и sodo или chroot, просто набор ограничений чуть шире и полней.

Начнём: получаем docker-образ

Существует github для docker: хранилище готовых образов. Вы можете там хранить и свои. Возьмём оттуда готовый образ какой-нибудь широко известной операционной системы:

$ sudo docker pull ubuntu
....
Status: Downloaded newer image for ubuntu:latest

Мы можем убедиться, что образ приехал:

$ sudo docker image ls
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
ubuntu              latest              ea4c82dcd15a        8 days ago          85.8MB

Теперь мы можем запустить любой бинарь из этого образа. Да, это звучит и выглядит, как запуск виртуальной машины и сбивает с толку. На самом деле, мы помним, что это чуть более умный chroot/sudo.

$ sudo docker run -it --name run-bash ubuntu /bin/bash
root@9198a671a22d:/# ls
bin  boot  dev  etc  home  lib  lib64  media  mnt  opt  proc  root  run  sbin  srv  sys  tmp  usr  var
root@9198a671a22d:/# ps uaxwwwf
USER   PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root     1  0.3  0.1  18508  3388 pts/0    Ss   06:19   0:00 /bin/bash
root    14  0.0  0.1  34400  2992 pts/0    R+   06:19   0:00 ps uaxwwwf
root@9198a671a22d:/# mc
bash: mc: command not found

Что мы видим:

Давайте посмотрим на наш контейнер снаружи:

$ sudo docker ps
CONTAINER ID   IMAGE   COMMAND      CREATED        STATUS         PORTS       NAMES
9198a671a22d   ubuntu  "/bin/bash"  6 minutes ago  Up 6 minutes               run-bash

Мы видим, что контейнер сделан на основе образа ubuntu, какая запущена команда, видим и имя контейнера из опции --name.

Если мы выйдем из контейнера:

root@9198a671a22d:/# exit

То он никуда не пропадёт, а просто остановится:

$ sudo docker ps -a
CONTAINER ID   IMAGE   COMMAND      CREATED        STATUS                        PORTS    NAMES
9198a671a22d   ubuntu  "/bin/bash"  9 minutes ago  Exited (127) 58 seconds ago            run-bash

Обратите внимание на опцию -a, без неё мы бы не увидели остановленный контейнер.

docker: Error response from daemon: Conflict

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

$ sudo docker run -it --name run-bash ubuntu /bin/bash
docker: Error response from daemon: Conflict. The container name "/run-bash" is already in use by container "9198a671a22dfa377fdf1b7a9e9d1cbfcef1f115d25bc87f2b576d22341b2228". You have to remove (or rename) that container to be able to reuse that name. See 'docker run --help'.

Вы можете перезапустить этот контейнер, но сейчас давайте просто удалим его:

$ sudo docker rm 9198a671a22dfa377fdf1b7a9e9d1cbfcef1f115d25bc87f2b576d22341b2228
9198a671a22dfa377fdf1b7a9e9d1cbfcef1f115d25bc87f2b576d22341b2228

При этом вы не удалили никакие данные или процессы, вы удалили только мета-информацию о песочнице, в которой бежал ваш bash.

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

$ sudo docker ps -aq --no-trunc -f status=exited | xargs sudo docker rm

Теперь вы можете снова запустить контейнер.

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

Файловая система docker

Если мы снова запустим наш контейнер и установим в него mc:

$ sudo docker run -it --name run-bash ubuntu /bin/bash
root@a0680cfe0473:/# apt-get update
root@a0680cfe0473:/# apt-get install mc

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

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

$ sudo docker commit run-bash ubuntu-mc:v1

Посмотри, что получилось:

$ sudo docker image ls
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
ubuntu-mc           v1                  e417912f9626        55 seconds ago      206MB
ubuntu              latest              ea4c82dcd15a        8 days ago          85.8MB

Видим, что

Теперь мы можем запуститься на новом образе и наш mc будет с нами

$ sudo docker run -it --name run-bash ubuntu-mc:v1 /usr/bin/mc

Здесь надо упомянуть про слои: файловая система докера слоёная, каждый слой хранится отдельно и один накладывается поверх другого. Для нас сейчас это не важно, но для оптимальной работы с докером, это хорошо бы знать. Если вы не знаете, почитайте про это. Я же пойду дальше.

Запускаем сервер в docker

Давайте создадим контейнер с крошечным сервером.

Запустимся на базовом образе и поставим nodejs:

$ sudo docker run -it --name run-bash ubuntu /bin/bash
root@ec8d181aaab3:/# apt-get update
root@ec8d181aaab3:/# apt-get install nodejs

Набросаем http-сервер, который возвращает на любой запрос текущее время:

root@ec8d181aaab3:/# cat >server.js
require("http").createServer((a, b) => {
    console.log(`${a.method} ${a.url}`);
    b.end(`${new Date()}\n`);
}).listen(80);
console.log('Started...');

Запустим:

root@ec8d181aaab3:/# node server.js
Started...

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

Как сделать ssh-дотуп в docker?

Поднять в docker ssh-сервер не сложнее, чем в обычной системе, но вам не надо этого делать.

Вспомните: docker-контейнер, это не виртуальная машина. Это просто отдельное пространство процессов.

Чтобы попасть в это пространство, вам надо просто подселить туда ещё один процесс. Это происходит точно так же, как и запуск первого процесса.

Что бы запустить любой процесс, в том числе shell, в существующем контейнере, существует команда docker exec.

Видим, что наш контейнер с сервером запущен:

$ sudo docker ps
CONTAINER ID  IMAGE   COMMAND      CREATED         STATUS         PORTS  NAMES
ec8d181aaab3  ubuntu  "/bin/bash"  12 minutes ago  Up 12 minutes         run-bash

Мы можем просто запустить в нём ещё один shell, аналогично команде docker run:

$ sudo docker exec -it run-bash /bin/bash
root@ec8d181aaab3:/# ps auxwwwf
USER   PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root  2861  0.5  0.1  18508  3400 pts/1    Ss   09:04   0:00 /bin/bash
root  2872  0.0  0.1  34400  3000 pts/1    R+   09:04   0:00  \_ ps auxwwwf
root     1  0.0  0.1  18504  3404 pts/0    Ss   08:49   0:00 /bin/bash
root  2855  0.3  1.5 922040 29644 pts/0    Sl+  08:58   0:01 node server.js
root@ec8d181aaab3:/# apt-get install iproute2
root@ec8d181aaab3:/# ss -ltpn
State   Recv-Q  Send-Q  Local Address:Port  Peer Address:Port
LISTEN  0       128     *:80                *:*                 users:(("node",pid=3199,fd=12))
root@ec8d181aaab3:/# apt-get install curl
root@ec8d181aaab3:/# curl localhost
Sat Oct 27 2018 09:12:46 GMT+0000 (UTC)
root@ec8d181aaab3:/#

Т.е. мы

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

Как включить проброс порта в уже запущенный docker

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

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

Итак. Чтобы не потерять нашу работу по настройке контейнера, делаем commit и сохраняем образ:

$ sudo docker commit run-bash ubuntu-node:latest
$ sudo docker image ls
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
ubuntu-node         latest              bd236e434f30        8 seconds ago       233MB
ubuntu-mc           v1                  e417912f9626        3 hours ago         206MB
ubuntu              latest              ea4c82dcd15a        8 days ago          85.8MB

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

Останавливаем контейнер. Я просто прибью все контейнеры командой:

$ sudo docker ps -aq --no-trunc | xargs sudo docker rm

Запускаем сразу наш сервер и пробрасываем порт 80 наружу. Снаружи я ему назначил номер 9911:

$ sudo docker run -it -p 9911:80 --name run-bash ubuntu-node /usr/bin/node server.js
Started...

Теперь мы можем обратиться к серверу, бегущему в контейнере, снаружи:

$ curl localhost:9911/
Sat Oct 27 2018 09:30:24 GMT+0000 (UTC)

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

Осталось немного: показать, как можно автоматизировать все эти шаги.

Dockerfile

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

Созадём два файла: server.js (он есть выше) и Dockerfile:

FROM ubuntu
RUN apt-get update
RUN apt-get install -y nodejs
COPY server.js /
CMD ["/usr/bin/node", "server.js"]
EXPOSE 80

Мы описали все шаги, необходимые для запуска нашего сервера (всё, что мы делали руками до команды commit). Замечаний заслуживают, пожалуй, только: - -y в третьей строке — нам не надо лишних вопросов - COPY — мы копируем файл с хост-системы в контейнер (да, снова в корень, не очень красиво, но сейчас это не принципиально) - CMD — запускает команду с аргументами в качеств процесса с PID=1 (обычно, в контейнере только этот процесс и существует) - EXPOSE — не совершает никаких действий, но декларирует, что порт 80 не мешало бы пробросить наружу.

Теперь мы можем создать контейнер, согласно описанию:

$ sudo docker build -t node-server:latest .
....
Successfully tagged node-server:latest

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

Теперь у нас появился образ node-server:

$ sudo docker image ls
REPOSITORY    TAG     IMAGE ID       CREATED         SIZE
node-server   latest  141ca8730ae0   53 seconds ago  170MB
ubuntu-node   latest  bd236e434f30   41 minutes ago  233MB
ubuntu-mc     v1      e417912f9626   3 hours ago     206MB
ubuntu        latest  ea4c82dcd15a   8 days ago      85.8MB

И теперь мы можем запустить контейнер, как все нормальные люди (без -it, без указания команды…):

$ sudo docker run -p 9911:80 --name server node-server
Started...

Вы уже знаете, как посмотреть, что он бежит (docker ps), остановить и перезапустить его, как подключиться к нему для дебага… И конечно, вы можете обратиться к нему снаружи:

$ curl localhost:9911/
Sat Oct 27 2018 10:11:04 GMT+0000 (UTC)

Здесь не рассказано про множество мелких деталей. Рекомендую почитать про все команды докера и про Dockerfile. Но про это есть множество материалов, статей, лекций и выступлений.

Я же, напоследок, расскажу, как удалить всё, что мы наделали:

$ sudo docker system prune -a

Будьте осторожны с этой командой.

Успехов!