Docker запускает программы в контейнерах. Контейнер
— обычный процесс Linux, которому ядро выделило свои пространства имён и
cgroup (cgroups-namespaces): своё дерево процессов, сеть, файловую
систему. Файлы и команду запуска контейнер берёт из
образа.
Работает это так: служба docker (демон dockerd) создаёт и держит
контейнеры, команда docker отправляет ей запросы через сокет
/var/run/docker.sock. Писать в сокет могут root и группа docker.
На машине тренажёра student не входит в группу docker, поэтому все
команды — через sudo. Без него:
student@ubuntu:~$ docker ps
permission denied while trying to connect to the docker API at
unix:///var/run/docker.sock
Служба в образе машины выключена: подготовка урока или лабы включает её
сама, руками — sudo systemctl enable --now docker.
Образ и контейнер
Образ — шаблон, контейнер — запущенный из него экземпляр со своим именем.
Из одного образа можно создать много контейнеров, у каждого свой слой для
записи. docker images показывает образы, docker ps — работающие
контейнеры, docker ps -a — все, включая остановленные. Полная таблица
ps -a широкая (ID, образ, команда, время, статус, порты, имя), нужные
столбцы выбирает --format:
student@ubuntu:~$ sudo docker ps -a --format '{{.Names}} {{.Status}}'
r4 Exited (3) 6 seconds ago
r3 Up 8 seconds
r2 Up 9 seconds
docker run — это docker create и docker start разом. -d оставляет
контейнер в фоне, --name даёт имя (не меньше двух символов: на a
Docker ответит Invalid container name), --rm удаляет контейнер после
выхода. Без --name Docker придумает имя сам.
Главный процесс и статус
Контейнер живёт, пока жив его главный процесс —
тот, что запустила команда образа или docker run. Внутри контейнера у
него PID 1. Процесс завершился — контейнер остановлен, его код завершения
виден в статусе.
STATUS в docker ps -a |
Что с контейнером |
Up 9 seconds |
главный процесс работает |
Up 6 seconds (healthy) |
работает, проверка здоровья проходит |
Exited (3) 6 seconds ago |
процесс вышел с кодом 3 |
Exited (137) |
процесс убит сигналом KILL (137 = 128 + 9) |
Restarting (1) 1 second ago |
вышел с кодом 1, Docker запускает снова |
Created |
создан, но не запускался или запуск не удался |
Почему процесс вышел, говорит docker logs — всё, что главный процесс
писал в stdout и stderr, в том числе до падения:
student@ubuntu:~$ sudo docker logs --tail 2 web
httpd: can't change directory to '/www': No such file or directory
httpd: can't change directory to '/www': No such file or directory
docker logs -f — следить в реальном времени, -t — с метками времени,
--since 10m — за последние 10 минут. Логи хранятся, пока существует
контейнер.
Порты
Контейнер слушает порт на своём адресе во внутренней сети Docker. На
адресах хоста, в том числе localhost, этого порта нет, пока его не
опубликовать. Публикация порта -p 8080:80
пробрасывает порт 8080 хоста в порт 80 контейнера — сначала хост, потом
контейнер:
student@ubuntu:~$ sudo docker run -d --name app -p 8081:80 app:1.1
student@ubuntu:~$ sudo docker port app
80/tcp -> 0.0.0.0:8081
80/tcp -> [::]:8081
0.0.0.0 — порт открыт на всех адресах хоста. Только для своей машины —
-p 127.0.0.1:8081:80. Порт хоста может занять один контейнер или одна
служба: второй -p 8081:… получит port is already allocated.
Переменные окружения
-e ИМЯ=значение передаёт переменную
главному процессу, --env-file app.env — все строки ИМЯ=значение из
файла. -e в той же команде важнее файла:
student@ubuntu:~$ cat app.env
MODE=prod
LEVEL=2
student@ubuntu:~$ sudo docker run --rm --env-file app.env -e LEVEL=3 \
busybox sh -c 'echo $MODE $LEVEL'
prod 3
Окружение, порты и тома задаются при создании контейнера. У
работающего контейнера их не поменять: удали его и создай заново с
новыми ключами. Посмотреть, с чем он создан, —
docker inspect -f '{{json .Config.Env}}' web.
Политика перезапуска
Политика перезапуска говорит Docker, запускать ли
контейнер снова, когда главный процесс вышел или перезапустилась сама
служба docker. Задаётся --restart при создании, у готового контейнера —
docker update --restart unless-stopped web.
--restart |
Главный процесс вышел сам |
no (по умолчанию) |
контейнер остаётся Exited |
on-failure |
запускает снова, если код ≠ 0; on-failure:3 — не больше 3 раз |
always |
запускает снова при любом коде |
unless-stopped |
запускает снова при любом коде |
always и unless-stopped расходятся на ручной остановке. После
docker stop контейнер стоит при любой политике. Перезапуск службы
docker поднимает контейнеры с always, а с unless-stopped — только
те, что не останавливали руками. Контейнер с no, работавший в момент
перезапуска, остаётся Exited (137). Так это выглядит на машине:
student@ubuntu:~$ sudo docker stop r1 r2
student@ubuntu:~$ sudo systemctl restart docker
student@ubuntu:~$ sudo docker ps -a --format '{{.Names}} {{.Status}}'
r4 Exited (3) 31 seconds ago # on-failure:3, исчерпал попытки
r3 Exited (137) 4 seconds ago # no: служба его остановила
r2 Up 3 seconds # always
r1 Exited (137) 15 seconds ago # unless-stopped, остановлен руками
Служба docker включена (systemctl enable docker) — после перезагрузки
машины контейнеры поднимаются по тем же правилам. docker stop для
always и unless-stopped действует до перезапуска службы, docker rm
— навсегда.
Падающий контейнер с always попадает в цикл перезапусков: статус
Restarting, счётчик RestartCount растёт, пауза между попытками
каждый раз примерно удваивается. Причину ищи в docker logs.
Тома и bind mount
Всё, что контейнер пишет в свои файлы, лежит в его
слое и удаляется с docker rm.
Данные, которые должны пережить пересоздание контейнера, хранят снаружи:
- том — каталог под управлением Docker:
-v data:/var/lib/app. Тома нет — docker run создаст его сам;
- bind mount — каталог хоста:
-v /srv/web:/www.
Путь слева начинается с / — значит, это каталог хоста.
student@ubuntu:~$ sudo docker volume create data1
student@ubuntu:~$ sudo docker volume inspect -f '{{.Mountpoint}}' data1
/var/lib/docker/volumes/data1/_data
student@ubuntu:~$ sudo docker run --rm -v data1:/data busybox \
sh -c 'echo hi > /data/f'
student@ubuntu:~$ sudo docker run --rm -v data1:/data busybox \
cat /data/f
hi
| Что |
Том |
Bind mount |
| Где файлы |
/var/lib/docker/volumes/<имя>/_data |
там, где укажешь |
| Пустой при первом подключении |
получает файлы образа из этого пути |
закрывает их, в контейнере пусто |
| Владелец и права |
как в образе |
как на хосте, числами |
| Удаление |
docker volume rm |
вручную, как обычный каталог |
Только для чтения — :ro в конце: -v /srv/web:/www:ro; запись внутри
кончится Read-only file system. В bind mount контейнер видит владельцев
хоста числами: файл student — 1000 1000. Процесс в контейнере от другого
пользователя упрётся в права так же, как на хосте.
docker volume rm не удалит том, пока его использует хоть один
контейнер, даже остановленный.
Безымянный том появляется, когда у пути нет имени тома: -v /data или
инструкция VOLUME /data в dockerfile образа. Каждый docker run
без -v создаёт новый пустой том с хэшем вместо имени, и данных
прежнего контейнера в нём нет:
student@ubuntu:~$ sudo docker rm -f va
student@ubuntu:~$ sudo docker run -d --name vb volimg:1
student@ubuntu:~$ sudo docker volume ls
DRIVER VOLUME NAME
local 77a8347bd7fbf9a1424251442a1d212edaab5cb8e2580b5a00b9e…
local 422da370c4cbf32ad7280434ea9bbad20f13bc25ba519945002353…
Первый том — от удалённого va: docker rm его оставил, данные лежат
в /var/lib/docker/volumes/77a8…/_data. Подключить его снова можно по
этому имени: -v 77a8347b…:/data. docker rm -v и docker run --rm
удаляют безымянные тома вместе с контейнером.
Сети
У каждого контейнера свой сетевой интерфейс — конец пары
veth, второй конец в мосту на хосте. Docker создаёт три сети:
student@ubuntu:~$ sudo docker network ls
NETWORK ID NAME DRIVER SCOPE
e92e175fe67b bridge bridge local
ea5b0c89cc6d host host local
1a37c7f6819b none null local
Без --network контейнер попадает в bridge (мост docker0,
172.17.0.0/16). Там контейнеры видят друг друга только по адресу —
имён нет:
student@ubuntu:~$ sudo docker exec web1 ping -c1 -W1 web2
ping: bad address 'web2'
В сети по умолчанию в /etc/resolv.conf контейнера — DNS-серверы хоста.
У машины тренажёра там только локальный 127.0.0.53, который из
контейнера недоступен, и Docker подставляет 8.8.8.8 и 8.8.4.4.
В своей сети имена работают:
DNS Docker на 127.0.0.11 отвечает именем
контейнера.
student@ubuntu:~$ sudo docker network create appnet
student@ubuntu:~$ sudo docker run -d --name app1 --network appnet \
busybox sleep 600
student@ubuntu:~$ sudo docker run -d --name db1 --network appnet \
busybox sleep 600
student@ubuntu:~$ sudo docker exec app1 ping -c1 -W1 db1 | head -1
PING db1 (172.18.0.3): 56 data bytes
docker network connect appnet web1 добавляет работающий контейнер во
вторую сеть. Публиковать порт (-p) нужно только контейнеру, к которому
ходят с хоста; остальные общаются внутри сети. docker-compose создаёт
такую сеть для каждого проекта сам.
Лимиты памяти и CPU
Без лимитов контейнер может занять всю память и все процессоры машины.
docker stats в столбце LIMIT показывает тогда память всей машины —
458.2MiB на машине тренажёра. Лимиты ставит ядро через cgroup
контейнера (cgroups-namespaces):
student@ubuntu:~$ sudo docker run -d --name m1 --memory 64m --cpus 0.5 \
busybox sleep 600
student@ubuntu:~$ sudo docker stats --no-stream --format \
'table {{.Name}}\t{{.MemUsage}}\t{{.MemPerc}}' m1
NAME MEM USAGE / LIMIT MEM %
m1 572KiB / 64MiB 0.87%
| Ключ |
Что ограничивает |
--memory 64m |
память процессов контейнера |
--memory-swap |
память и своп вместе; без ключа — вдвое больше --memory |
--cpus 0.5 |
половина одного процессора |
В inspect лимиты лежат в байтах и миллиардных долях процессора:
sudo docker inspect -f '{{.HostConfig.Memory}}' m1
sudo docker inspect -f '{{.HostConfig.MemorySwap}}' m1
sudo docker inspect -f '{{.HostConfig.NanoCpus}}' m1
Своп на машине тренажёра выключен, поэтому --memory — весь доступный
контейнеру объём. Процесс, которому его мало, убивает
OOM killer: контейнер выходит с кодом 137, в
inspect — OOMKilled: true.
student@ubuntu:~$ sudo docker run --name oom1 --memory 16m busybox \
sh -c 'x=$(head -c 67108864 /dev/zero | tr "\0" a)'
student@ubuntu:~$ echo $?
137
student@ubuntu:~$ sudo docker inspect -f '{{.State.OOMKilled}}' oom1
true
Код 137 бывает и после docker stop, если процесс не завершился за 10
секунд, — различает их только OOMKilled. Лимиты — исключение из
правила «задаётся при создании»: docker update --memory 32m
--memory-swap 32m m1 меняет их у работающего контейнера.
Пользователь процесса
Главный процесс по умолчанию работает от root контейнера, UID 0. Ядро у
контейнера и хоста одно, и UID тоже общие: в ps на хосте такой
процесс — root. Пользователя задаёт USER в dockerfile или
--user при запуске:
--user |
id внутри контейнера |
1000 |
uid=1000 gid=0(root) groups=0(root) |
1000:1000 |
uid=1000 gid=1000 groups=1000 |
nobody |
uid=65534(nobody) gid=65534(nobody) … |
web |
ошибка: unable to find user web: no matching entries in passwd file |
Имя Docker ищет в /etc/passwd образа, число подходит любое. --user
1000 без группы оставляет группу 0 — root; группу пиши явно:
--user 1000:1000.
UID 1000 в контейнере — это student на хосте: в ps на хосте процесс
sleep 600 с --user 1000 принадлежит student. Файлы, которые процесс
создаёт в bind mount, получают его UID:
student@ubuntu:~$ ls -ln ~/u
total 0
-rw-r--r-- 1 1000 1000 0 Oct 20 15:03 by1000
-rw-r--r-- 1 0 0 0 Oct 20 15:03 byroot
byroot создал контейнер от root — student на хосте не сможет его
изменить. Процесс не от root упрётся и в права внутри образа:
touch /etc/x — Permission denied.
docker inspect
docker inspect печатает всё о контейнере, образе, томе или сети в JSON.
Одно поле достаёт -f с шаблоном Go: точка — корень JSON, дальше путь по
ключам.
sudo docker inspect -f '{{.State.Status}} {{.State.ExitCode}}' web
sudo docker inspect -f '{{.RestartCount}}' web
sudo docker inspect -f '{{.HostConfig.RestartPolicy.Name}}' web
sudo docker inspect -f '{{json .Mounts}}' web
sudo docker inspect -f '{{json .Config.Env}}' web
sudo docker inspect -f '{{.State.Pid}}' web
sudo docker inspect web | jq '.[0].State'
| Поле |
Что в нём |
.State.Status |
running, exited, restarting, created |
.State.ExitCode |
код завершения главного процесса |
.State.OOMKilled |
true — процесс убит за превышение памяти |
.State.Health.Status |
healthy, unhealthy, starting |
.RestartCount |
сколько раз Docker перезапускал контейнер |
.HostConfig.RestartPolicy.Name |
политика перезапуска |
.Mounts |
тома и bind mount: что, куда, только ли чтение |
.Config.Env |
переменные окружения |
.NetworkSettings.Networks |
сети и адреса |
Перебор словаря или списка — range. Адреса контейнера во всех сетях:
sudo docker inspect -f \
'{{range .NetworkSettings.Networks}}{{.IPAddress}} {{end}}' web1
JSON целиком удобнее разбирать jq.