Мне приходилось сталкиваться с 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, но уже с полным пониманием происходящего.
Не вдаваясь в историю и в детали реализации на разных операционных
системах, скажу, что сейчас в Linux docker использует не виртуализацию
(как многие думают), а средства ядра, позволяющие создавать изолированные
группы процессов. Т.е. запуская "виртуальную машину" (пишу это в кавычках),
docker делает всего несколько системных вызовов и ядро создаёт для нового
процесса отдельное пространство PID-ов, отдельную виртуальную сеть,
отдельный набор ограничений по ресурсам. Процесс "запущенный в docker",
на самом деле находится не в какой-то виртуальной машине (нет никакого эмулятора
настоящей машины, никакой виртуальной сущности), он запущен
на той же машине, тем же ядром, просто ядро ассоциирует его со специфическим
набором настроек. Это почти то же самое, что и sodo
или chroot
,
просто набор ограничений чуть шире и полней.
Существует 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
, без неё мы бы не увидели остановленный контейнер.
Это самая частая ошибка, с которой сталкиваются начинающие пользователи. Если сейчас попытаться снова запустить наш 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
Теперь вы можете снова запустить контейнер.
Мы научились уверенно запускать контейнеры, давайте разберёмся с файлами.
Если мы снова запустим наш контейнер и установим в него 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
Видим, что
ubuntu-mc
: это и есть базовая Ubuntu, плюс mc
, который мы установили самиТеперь мы можем запуститься на новом образе и наш mc
будет с нами
$ sudo docker run -it --name run-bash ubuntu-mc:v1 /usr/bin/mc
Здесь надо упомянуть про слои: файловая система докера слоёная, каждый слой хранится отдельно и один накладывается поверх другого. Для нас сейчас это не важно, но для оптимальной работы с докером, это хорошо бы знать. Если вы не знаете, почитайте про это. Я же пойду дальше.
Давайте создадим контейнер с крошечным сервером.
Запустимся на базовом образе и поставим 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...
А сейчас сделаем небольшую паузу и уделим время двум важным вопросам, которыми задаются люди, когда не до конца понимают, что такое докер.
Поднять в 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:/#
Т.е. мы
node
,node
слушает на порту 80,Возвращаясь к первоначальному вопросу, обратите внимание, никакой ssh-доступ в 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
содержит сценарий создания образа. Он позволяет делать некоторые дополнительные
выкрутасы, на которых я не будут тут останавливаться. Про это как раз написано везде хорошо
и много. Я лишь покажу, как легко можно автоматизировать все действия, которые мы только
что проделали руками.
Создаём два файла: 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
Будьте осторожны с этой командой.
Успехов!