С чего начать Первые шаги трек · 15 ступеней
Linux Labs

Docker — контейнеры: запуск, порты, тома, сети

21 мин Docker 29.1 · Ubuntu 24.04

Docker запускает программы в контейнерах — изолированных процессах из образов — и управляет их портами, томами и сетями.

Шпаргалка

12 команд Версия для печати
  • sudo systemctl enable --now docker включить службу
  • sudo docker run -d --name web -p 8080:80 nginx:alpine фоном
  • sudo docker ps -a все контейнеры и их статус
  • sudo docker logs --tail 20 web вывод главного процесса
  • sudo docker exec -it web sh оболочка внутри
  • sudo docker stop web; sudo docker start web
  • sudo docker rm -f web удалить, даже работающий
  • sudo docker port web опубликованные порты
  • sudo docker inspect -f '{{.State.Status}}' web одно поле
  • sudo docker volume ls тома
  • sudo docker network ls сети
  • sudo docker stats --no-stream CPU и память контейнеров

Обзор

Docker запускает программы в контейнерах. Контейнер — обычный процесс Linux, которому ядро выделило свои пространства имён и cgroup (cgroups-namespaces): своё дерево процессов, сеть, файловую систему. Файлы и команду запуска контейнер берёт из образа.

Работает это так: служба docker (демон dockerd) создаёт и держит контейнеры, команда docker отправляет ей запросы через сокет /var/run/docker.sock. Писать в сокет могут root и группа docker.

На машине тренажёра student не входит в группу docker, поэтому все команды — через sudo. Без него:

text
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:

text
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, в том числе до падения:

text
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 контейнера — сначала хост, потом контейнер:

text
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 в той же команде важнее файла:

text
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). Так это выглядит на машине:

text
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. Путь слева начинается с / — значит, это каталог хоста.
text
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 создаёт новый пустой том с хэшем вместо имени, и данных прежнего контейнера в нём нет:

text
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 создаёт три сети:

text
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). Там контейнеры видят друг друга только по адресу — имён нет:

text
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 отвечает именем контейнера.

text
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):

text
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 лимиты лежат в байтах и миллиардных долях процессора:

bash
sudo docker inspect -f '{{.HostConfig.Memory}}' m1      # 67108864
sudo docker inspect -f '{{.HostConfig.MemorySwap}}' m1  # 134217728
sudo docker inspect -f '{{.HostConfig.NanoCpus}}' m1    # 500000000

Своп на машине тренажёра выключен, поэтому --memory — весь доступный контейнеру объём. Процесс, которому его мало, убивает OOM killer: контейнер выходит с кодом 137, в inspect — OOMKilled: true.

text
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:

text
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, дальше путь по ключам.

bash
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     # PID на хосте
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. Адреса контейнера во всех сетях:

bash
sudo docker inspect -f \
  '{{range .NetworkSettings.Networks}}{{.IPAddress}} {{end}}' web1

JSON целиком удобнее разбирать jq.

Конфигурация

Служба настраивается файлом /etc/docker/daemon.json — JSON без комментариев:

json
{
  "log-driver": "json-file",
  "log-opts": {"max-size": "10m", "max-file": "3"},
  "data-root": "/srv/docker"
}

Применить — sudo systemctl restart docker. Ошибка в JSON не даст службе стартовать: смотри journalctl -u docker. Что применилось:

bash
sudo docker info -f '{{.DockerRootDir}} {{.LoggingDriver}}'

На машине тренажёра это /var/lib/docker json-file. Перезапуск службы останавливает все контейнеры. Обратно поднимутся те, кого поднимет политика перезапуска (раздел выше). data-root меняй на пустом каталоге: старые образы и тома служба сама не перенесёт.

Рецепты

4

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

bash
sudo docker ps -a --filter name=web
sudo docker logs --tail 30 web
sudo docker inspect -f '{{.State.ExitCode}} {{.State.OOMKilled}}' web

Код 0 — команда просто закончилась: для службы это значит, что она ушла в фон. 137 — процесс убит KILL: нехватка памяти (OOMKilled true) или docker stop, на который процесс не ответил. 1, 2 и другие — ошибка самой программы, текст в логах.

Пересоздать контейнер с новыми ключами

bash
sudo docker inspect -f '{{json .Config.Env}} {{json .Mounts}}' web
sudo docker rm -f web
sudo docker run -d --name web --restart unless-stopped \
  -p 8080:80 -v data:/data -e MODE=prod app:1.1

Данные в томе переживут пересоздание, данные в слое контейнера — нет.

Найти контейнер среди процессов хоста

text
student@ubuntu:~$ sudo docker inspect -f '{{.State.Pid}}' web1
2055
student@ubuntu:~$ ps -o pid,user,cmd -p 2055
    PID USER     CMD
   2055 root     sleep 600

Команда в контейнере из скрипта

docker exec -it нужен терминал. В скрипте и конвейере ключ -t даёт ошибку the input device is not a TTY — пиши без -t: docker exec web cat /etc/hosts.

Неполадки

11

permission denied … docker.sock

Пользователь не в группе docker. Запускай через sudo. Членство в группе даёт права root на машине — см. «Опасно».

Cannot connect to the Docker daemon

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

Служба не запущена: sudo systemctl enable --now docker. Если ты останавливал только docker.service, systemd предупредит its triggering units are still active: docker.socket — первая же команда docker поднимет службу снова через сокет.

port is already allocated

Bind for 0.0.0.0:8081 failed: port is already allocated — порт хоста занят другим контейнером или службой: sudo ss -tlnp | grep :8081 (ss). Контейнер при этом уже создан — удали его docker rm и запусти с другим портом слева.

The container name … is already in use

Контейнер с таким именем уже есть, хотя бы остановленный (docker ps -a). Удали старый docker rm или выбери другое имя.

Контейнер сразу Exited (0)

Главный процесс закончился без ошибки. Так ведёт себя служба, которая ушла в фон: httpd без -f, nginx без daemon off. Главный процесс должен работать на переднем плане.

docker stop ждёт 10 секунд и даёт 137

docker stop шлёт SIGTERM и ждёт 10 секунд. Процесс с PID 1 без своего обработчика SIGTERM его не получает — так ведут себя sleep и busybox httpd, — и Docker добивает его KILL: Exited (137). Для таких контейнеров docker stop -t 1 или docker rm -f.

container … is not running

docker exec работает только в запущенном контейнере. Остановленный — docker start, а если он падает сразу, читай docker logs.

docker logs | grep не находит строку

docker logs отдаёт вывод контейнера двумя потоками: что процесс писал в stderr, идёт в stderr. Ошибки программ обычно там, и конвейер их не видит. Склей потоки: sudo docker logs web 2>&1 | grep error.

Данные пропали после пересоздания

Данные лежали в слое контейнера. Подключай том: -v data:/путь. Старый том никуда не делся, если контейнер был с ним: docker volume ls.

Контейнеры не видят друг друга по имени

Оба в сети по умолчанию bridge: ping: bad address. Создай свою сеть и подключи оба (docker network connect).

volume is in use / has active endpoints

Том или сеть нужны контейнеру, хотя бы остановленному. Список — docker ps -a --filter volume=data1 или --filter network=appnet; удали контейнеры, потом том или сеть.

Опасно

3

Группа docker = root

Кто может писать в docker.sock, тот запустит контейнер с -v /:/host и получит весь диск хоста от root. Добавлять пользователя в группу docker — то же, что дать ему sudo без пароля.

docker rm -f и данные в слое контейнера

rm -f останавливает и удаляет контейнер разом, вместе с его слоем. Всё, что контейнер записал не в том или bind mount, пропадёт.

docker volume prune -a и docker system prune

docker volume prune удаляет безымянные тома, которые не нужны ни одному контейнеру; с -a — и именованные. Том, чей контейнер ты как раз пересоздаёшь, в этот момент «не нужен никому» — данные удалятся безвозвратно. Перед чисткой проверь docker volume ls.

Термины

8
контейнер container
Процесс, запущенный из образа в своих пространствах имён: со своим именем, файловой системой и статусом в docker ps -a. Живёт, пока жив его главный процесс. Ещё встречается в: Образы контейнеров, docker compose, Dockerfile, Load average: сколько задач ждёт процессор и диск и ещё 9
главный процесс контейнера container main process
Процесс, который запускает команда образа (CMD, ENTRYPOINT) или docker run; внутри контейнера у него PID 1. Он завершился — контейнер переходит в Exited с его кодом завершения.
публикация порта port publishing
Проброс порта хоста в контейнер: -p 8080:80 — запросы на порт 8080 хоста попадают на порт 80 контейнера. Без неё служба в контейнере доступна только из сети Docker.
политика перезапуска restart policy
Правило, по которому Docker сам запускает остановившийся контейнер: no, on-failure, always или unless-stopped. Задаётся ключом --restart при создании или командой docker update. Ещё встречается в: docker compose
том (volume)
Каталог под управлением Docker (/var/lib/docker/volumes/<имя>/_data), подключённый в контейнер. Удаление контейнера его не трогает: данные живут, пока не удалят сам том. Ещё встречается в: docker compose, Dockerfile, Логические тома LVM, Образы контейнеров и ещё 24
bind mount
Каталог или файл хоста, подключённый внутрь контейнера по пути: -v /srv/web:/www. Контейнер видит те же файлы с теми же числовыми владельцами и правами. Ещё встречается в: docker compose
пользовательская сеть Docker user-defined network
Сеть, созданная docker network create или compose. Контейнеры в ней находят друг друга по имени через DNS Docker (127.0.0.11); в сети по умолчанию bridge имён нет. Ещё встречается в: docker compose
DNS Docker Docker DNS
DNS-сервер Docker на 127.0.0.11 в пользовательских сетях: отвечает именами контейнеров и сервисов compose своей сети. В сети bridge по умолчанию его нет — там DNS-серверы хоста.

См. также

  • container-images — образ, слои, теги, digest, docker load.
  • dockerfile — как собрать свой образ.
  • docker-compose — несколько контейнеров одним файлом.
  • cgroups-namespaces — на чём держится изоляция контейнера.
  • systemctl — служба docker: включить, перезапустить.
  • ss — кто занял порт хоста.
  • unix-sockets — docker.sock и права на него.
  • processes — контейнер среди процессов хоста.
Загрузка…