Модуль 02 — Образы и контейнеры

Скачать, запустить, заглянуть внутрь, остановить и убрать за собой

Прогресс курса Модуль 2 из 9

Что вы освоите в этом модуле

01

Имя образа

Полное имя образа состоит из частей, большинство из которых обычно опускают:

docker.io/library/nginx:1.28-alpine
└───┬───┘ └──┬──┘ └─┬─┘ └────┬────┘
 реестр   владелец  имя     тег

docker pull скачивает образ, не запуская его. Образ уже есть — Docker сверит его с реестром и ничего не скачает:

$ docker pull nginx:1.28-alpine
1.28-alpine: Pulling from library/nginx
Digest: sha256:a8b39bd9cf0f83869a2162827a0caf6137ddf759d50a171451b335cecc87d236
Status: Image is up to date for nginx:1.28-alpine
docker.io/library/nginx:1.28-alpine
$ docker image ls --filter reference=nginx --filter reference=alpine --filter reference=postgres --filter reference=golang
REPOSITORY   TAG           IMAGE ID       CREATED        SIZE
alpine       3.22          24ab38714e94   44 hours ago   8.3MB
postgres     17            82903352d6fa   2 days ago     454MB
golang       1.25          9bae33b05e4e   2 months ago   848MB
nginx        1.28-alpine   dc73b49f5124   7 months ago   62.1MB

Колонка CREATED — когда образ собран, а не когда скачан. Размеры отличаются на два порядка: alpine — 8 МБ, golang с компилятором — больше 800 МБ. Выбор базового образа — тема модуля 07.

02

Тег и дайджест, слои

Тег — изменяемая ссылка: завтра автор пересоберёт nginx:1.28-alpine с исправлением безопасности, и тот же тег будет указывать на другой образ. Неизменяемый адрес образа — дайджест, хеш SHA-256 его содержимого:

$ docker image ls --digests nginx
REPOSITORY   TAG           DIGEST                                                                    IMAGE ID       CREATED        SIZE
nginx        1.28-alpine   sha256:a8b39bd9cf0f83869a2162827a0caf6137ddf759d50a171451b335cecc87d236   dc73b49f5124   7 months ago   62.1MB
$ docker image inspect -f "{{.Os}}/{{.Architecture}}, слоёв: {{len .RootFS.Layers}}" nginx:1.28-alpine
linux/amd64, слоёв: 8

Образ по дайджесту запускается так: docker run nginx@sha256:a8b39b…. Это гарантирует, что запустится ровно тот образ, который вы проверили.

Образ состоит из слоёв. Каждый слой — изменения файловой системы от одного шага сборки. История показывает, какой командой создан каждый слой:

$ docker history nginx:1.28-alpine
IMAGE          CREATED        CREATED BY                                      SIZE      COMMENT
dc73b49f5124   7 months ago   RUN /bin/sh -c set -x     && apkArch="$(cat …   49.4MB    buildkit.dockerfile.v0
<missing>      7 months ago   ENV ACME_VERSION=0.3.1                          0B        buildkit.dockerfile.v0
<missing>      7 months ago   ENV NJS_RELEASE=1                               0B        buildkit.dockerfile.v0
<missing>      7 months ago   ENV NJS_VERSION=0.9.6                           0B        buildkit.dockerfile.v0
<missing>      7 months ago   CMD ["nginx" "-g" "daemon off;"]                0B        buildkit.dockerfile.v0
<missing>      7 months ago   STOPSIGNAL SIGQUIT                              0B        buildkit.dockerfile.v0
<missing>      7 months ago   EXPOSE map[80/tcp:{}]                           0B        buildkit.dockerfile.v0
…

Слои только для чтения и общие: если у десяти образов одна база alpine, на диске она хранится один раз и скачивается один раз. Строки размером 0B — метаданные (команда запуска, переменные, порт), они не добавляют файлов. Как слои появляются при сборке — в модуле 03.

03

Запуск в фоне: веб-сервер за минуту

Запустим nginx и откроем его порт на хосте:

$ docker run -d --name web -p 8080:80 nginx:1.28-alpine
05d954b8ef3323d45f89f92b551a98973f20b589faf7284e7ddb68e68bee1f98
$ docker ps
CONTAINER ID   IMAGE               COMMAND                  CREATED        STATUS        PORTS                                   NAMES
05d954b8ef33   nginx:1.28-alpine   "/docker-entrypoint.…"   1 second ago   Up 1 second   0.0.0.0:8080->80/tcp, :::8080->80/tcp   web
$ curl -sI http://localhost:8080 | head -3
HTTP/1.1 200 OK
Server: nginx/1.28.3
Date: Fri, 23 Oct 2026 17:08:45 GMT
ПараметрЧто делает
-dЗапустить в фоне (detached) и напечатать идентификатор. Без него терминал занят выводом контейнера
--name webСвоё имя вместо случайного; по имени удобно обращаться в остальных командах
-p 8080:80Публикация порта: ХОСТ:КОНТЕЙНЕР. Запрос на порт 8080 хоста попадёт на порт 80 контейнера

Обратите внимание на 0.0.0.0:8080: порт открыт на всех адресах хоста, то есть доступен из сети. На сервере это обычно не нужно — -p 127.0.0.1:8080:80 откроет порт только для самого хоста. Почему правила межсетевого экрана тут не спасают — в модуле 05.

Идентификатор контейнера можно сокращать до первых символов, пока сокращение однозначно: docker stop и четыре первых символа из колонки CONTAINER ID. Но имя удобнее.

04

Логи и команды внутри контейнера

Приложение в контейнере пишет журнал в стандартный вывод, Docker его сохраняет. docker logs показывает журнал, -f — следит за ним, как tail -f, --since 10m — только за последние 10 минут:

$ docker logs web 2>&1 | tail -3
2026/10/23 17:08:44 [notice] 1#1: start worker process 30
2026/10/23 17:08:44 [notice] 1#1: start worker process 31
172.17.0.1 - - [23/Oct/2026:17:08:45 +0000] "HEAD / HTTP/1.1" 200 0 "-" "curl/8.14.1" "-"

Последняя строка — наш запрос curl. Адрес 172.17.0.1 — хост, каким его видит контейнер.

docker exec запускает ещё один процесс в уже работающем контейнере. Узнаем версию nginx, посмотрим файлы сайта и заменим стартовую страницу:

$ docker exec web nginx -v
nginx version: nginx/1.28.3
$ docker exec web ls /usr/share/nginx/html
50x.html
index.html
$ docker exec web sh -c "echo \"<h1>Привет из контейнера</h1>\" > /usr/share/nginx/html/index.html"
$ curl -s http://localhost:8080
<h1>Привет из контейнера</h1>

Чтобы «войти» в контейнер и поработать в оболочке, нужен интерактивный режим: docker exec -it web sh. В образах на Debian есть bash, в Alpine — только sh. Во многих рабочих образах оболочки нет вовсе — это нормально и даже хорошо (модуль 07).

Правка файлов через exec годится для разбора проблемы, но не для настройки. Изменения живут в контейнере и пропадут при его пересоздании — следующий раздел. Настройки и содержимое попадают в контейнер через образ (модуль 03) или через тома (модуль 04).
05

Жизненный цикл контейнера

          docker create         docker start
  образ ───────────────▶ created ───────────▶ running ◀──┐
                                              │  │       │ docker start
                              docker stop,    │  │       │ docker restart
                              процесс завершился ▼       │
                                            exited ──────┘
                                              │
                                    docker rm ▼
                                           удалён

docker run — это create и start одной командой. Остановленный контейнер сохраняет свой слой записи: остановим и снова запустим — изменённая страница на месте:

$ docker stop web
web
$ docker ps -a --format "table {{.Names}}\t{{.Status}}"
NAMES     STATUS
web       Exited (0) Less than a second ago
$ docker start web
web
$ curl -s http://localhost:8080
<h1>Привет из контейнера</h1>

Удалить работающий контейнер Docker не даст; -f сначала остановит его. А новый контейнер из того же образа начинает с чистого листа — нашей страницы в нём нет:

$ docker rm web
Error response from daemon: cannot remove container "/web": container is running: stop the container before removing or force remove
$ docker rm -f web
web
$ docker run -d --name web -p 8080:80 nginx:1.28-alpine
d3f2b1176811aa335e20ce0d6da0760b4abd36b4cfc858f41b5c313398360f9c
$ curl -s http://localhost:8080 | grep -o "<title>.*</title>"
<title>Welcome to nginx!</title>
$ docker rm -f web
web

Это главное свойство, на котором строится работа с Docker: контейнер одноразовый. Обновить приложение — значит удалить контейнер и создать новый из нового образа. Всё, что должно пережить пересоздание, хранится вне контейнера.

Для одноразовых команд есть --rm: контейнер удалится сам, когда процесс завершится. Без него остановленные контейнеры копятся — в разделе 09 мы их уберём.

06

Интерактивный режим: -i и -t

Два параметра, которые почти всегда пишут вместе как -it, делают разное:

$ docker run --rm alpine:3.22 tty
not a tty
$ docker run --rm -t alpine:3.22 tty
/dev/pts/0
$ echo | docker run --rm -it alpine:3.22 tty
the input device is not a TTY

Без -i процесс не получит ввода, и оболочка сразу завершится. С -i, но без терминала, ввод можно подать конвейером — так в контейнер передают скрипт. Код выхода оболочки становится кодом docker run:

$ printf "cat /etc/alpine-release\nexit 5\n" | docker run --rm -i alpine:3.22 sh; echo "код: $?"
3.22.7
код: 5

Для работы руками — docker run -it --rm alpine:3.22 sh: вы в оболочке контейнера, exit — выход и удаление. В скриптах и cron -it не ставят: ввод там не терминал, и docker откажется запускать контейнер — последняя команда выше.

07

Коды выхода и docker stop

Код завершения главного процесса — код контейнера. Его видно в docker ps -a и в $? после docker run без -d:

$ docker run --name fail alpine:3.22 sh -c "echo работаю; exit 3"; echo "код: $?"
работаю
код: 3
$ docker ps -a --filter name=fail --format "{{.Names}}: {{.Status}}"
fail: Exited (3) Less than a second ago
$ docker rm fail
fail

docker stop посылает главному процессу сигнал SIGTERM — «пожалуйста, завершись» — и ждёт 10 секунд. Если процесс не завершился, Docker посылает SIGKILL, который убивает без разговоров. Посмотрим на sleep:

$ docker run -d --name sleeper alpine:3.22 sleep 300
3473c74ba77a38fcedd639e80d78e2fe3436e31c88fb0b5ced37395697283ce0
$ time docker stop sleeper
sleeper

real	0m10,186s
user	0m0,004s
sys	0m0,016s
$ docker inspect -f "{{.State.ExitCode}}" sleeper
137
$ docker run -d --name sleeper2 --init alpine:3.22 sleep 300
9ca2b86aa8d393f0ccd0c41a39a3e0256cb96c2b4ec1548d40d3e900cbe6937d
$ time docker stop sleeper2
sleeper2

real	0m0,132s
user	0m0,011s
sys	0m0,015s
$ docker inspect -f "{{.State.ExitCode}}" sleeper2
143
$ docker rm sleeper sleeper2
sleeper
sleeper2

Первый контейнер останавливался 10 секунд и получил код 137 — его убили. Причина в том, что sleep работает в контейнере как PID 1, а ядро не применяет к PID 1 действие сигнала «по умолчанию»: процесс, который сам не обрабатывает SIGTERM, просто его не замечает. С --init первым процессом становится маленький docker-init, который передаёт сигнал дальше, — остановка за доли секунды и код 143.

КодЧто значит
0Процесс завершился успешно
1–125Ошибка самого приложения: код, который вернула программа
125Ошибка Docker: неверный параметр docker run, конфликт имён
126, 127Команда в контейнере не запускается или не найдена
137 = 128 + 9Убит сигналом SIGKILL: не уложился в время docker stop, docker kill или нехватка памяти (модуль 07)
143 = 128 + 15Завершился по SIGTERM — штатная остановка

Время ожидания меняют параметром docker stop -t 30 — базам данных порой нужно больше 10 секунд, чтобы аккуратно закрыть файлы. Как писать приложения, которые правильно реагируют на SIGTERM, — в модулях 03 и 07.

08

Переменные окружения и перезапуск

Настройки передаются контейнеру переменными окружения, параметр -e ИМЯ=значение можно повторять:

$ docker run --rm -e GREETING=Привет -e USER_NAME=Анна alpine:3.22 sh -c "echo \$GREETING, \$USER_NAME"
Привет, Анна

Так задают пароль базы, адрес сервиса, уровень журнала: один образ — разные настройки на тестовом и боевом сервере. Много переменных удобнее хранить в файле и передавать через --env-file файл.

Политика перезапуска говорит Docker, что делать, когда процесс завершился:

ПолитикаКогда перезапускает
no (по умолчанию)Никогда
on-failure[:N]При ненулевом коде выхода, не больше N раз
alwaysВсегда, и после перезагрузки хоста; остановленный вручную — только после перезапуска службы Docker
unless-stoppedКак always, но остановленный вручную не поднимается и после перезагрузки

Контейнер, который падает с кодом 1, при on-failure:3 запустится четыре раза — первый запуск и три повтора; пауза перед повтором начинается со 100 мс и каждый раз удваивается:

$ docker run -d --name crashy --restart on-failure:3 alpine:3.22 sh -c "date +%T; exit 1"
012a87c10eb613d62fc0fb10d5a55f2cfb4b74aabedcf68aae8061d0e5dc4f5d
$ docker logs crashy
17:09:03
17:09:03
17:09:03
17:09:04
$ docker inspect -f "перезапусков: {{.RestartCount}}, статус: {{.State.Status}}" crashy
перезапусков: 3, статус: exited
$ docker rm crashy
crashy

Для служб на сервере обычно ставят --restart unless-stopped: контейнер переживёт перезагрузку хоста и падение приложения, но не воскреснет, если вы остановили его сами.

09

Всё о контейнере: inspect, port, stats

docker inspect выдаёт всё, что Docker знает о контейнере, — сотни строк JSON. Нужное поле достают шаблоном -f (язык шаблонов Go: поля через точку, json — вывести как JSON):

$ docker run -d --name web -p 8080:80 nginx:1.28-alpine
861c7e1b064660b8e17a0e6343a04ef57b6b36f6bf4621dbc1c7e61c69be8015
$ docker inspect -f "{{.NetworkSettings.IPAddress}}" web
172.17.0.2
$ docker inspect -f "{{json .HostConfig.PortBindings}}" web
{"80/tcp":[{"HostIp":"","HostPort":"8080"}]}
$ docker port web
80/tcp -> 0.0.0.0:8080
80/tcp -> [::]:8080
$ docker stats --no-stream --format "table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}" web
NAME      CPU %     MEM USAGE / LIMIT
web       0.00%     3.195MiB / 3.83GiB

docker stats без --no-stream обновляется каждую секунду, как top. Предел памяти здесь — вся память хоста: контейнер ничем не ограничен, пока ограничение не задано (модуль 07).

10

Уборка

Образ нельзя удалить, пока из него создан хоть один контейнер, даже остановленный:

$ docker rmi nginx:1.28-alpine
Error response from daemon: conflict: unable to remove repository reference "nginx:1.28-alpine" (must force) - container 861c7e1b0646 is using its referenced image dc73b49f5124
$ docker rm -f web
web

Остановленные контейнеры, неиспользуемые образы, тома и кэш сборки занимают диск. Для уборки есть команды prune, а docker system df показывает, сколько занято и сколько можно освободить:

$ docker run --name old1 alpine:3.22 true; docker run --name old2 alpine:3.22 true
$ docker container prune -f
Deleted Containers:
0fccf7e4d53b4e726351376c1ed6b100e3ce25db7fe656c1c3616d8f33208fde
12f0e2f8c458db66186478e306bb0298d68fdc269b8ec6722dfebffd4ab0b37d

Total reclaimed space: 0B
$ docker system df
TYPE            TOTAL     ACTIVE    SIZE      RECLAIMABLE
Images          16        0         3.172GB   3.172GB (100%)
Containers      0         0         0B        0B
Local Volumes   0         0         0B        0B
Build Cache     0         0         0B        0B
КомандаЧто удаляет
docker container pruneВсе остановленные контейнеры
docker image pruneОбразы без тега (остатки пересборок); с -a — все, из которых не создан контейнер
docker builder pruneКэш сборки
docker system pruneОстановленные контейнеры, неиспользуемые сети, образы без тега и кэш сборки
Остановленный контейнер может быть нужным: например, база, которую остановили на время обслуживания. prune удалит его без вопросов. На боевом сервере смотрите сначала docker ps -a, а docker system prune --volumes не запускайте вовсе, пока не прошли модуль 04: тома — это данные.

Ключевые выводы модуля

Запомните главное

  • Указывайте тег образа всегда; для точной воспроизводимости — дайджест @sha256:…
  • docker run -d --name … -p ХОСТ:КОНТЕЙНЕР -e … — основа запуска службы; на сервере — -p 127.0.0.1:…
  • logs, exec, inspect -f — первые инструменты разбора проблем
  • Контейнер одноразовый: при пересоздании изменения внутри него теряются
  • stop = SIGTERM, через 10 секунд SIGKILL; 137 — убит, 143 — завершился штатно
  • --restart unless-stopped для служб, --rm для разовых команд, prune — с осторожностью
Практические задания — на учебном портале. Задания, критерии оценивания, сдача работ и оценки преподавателя — в курсе на portal.nevabit.ru. Учётную запись выдаёт преподаватель.
Модуль 01: Контейнеры и установка Все модули Модуль 03: Dockerfile