Скачать, запустить, заглянуть внутрь, остановить и убрать за собой
docker stopПолное имя образа состоит из частей, большинство из которых обычно опускают:
docker.io/library/nginx:1.28-alpine
└───┬───┘ └──┬──┘ └─┬─┘ └────┬────┘
реестр владелец имя тег
docker.io (Docker Hub). Другие пишутся явно: cr.yandex/…, ghcr.io/…, registry.example.ru:5000/….library — официальные образы Docker Hub; у остальных это имя пользователя или организации: bitnami/postgresql.latest. Это не «последняя версия», а просто тег, который автор образа может никогда не обновлять. Указывайте тег всегда.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.
Тег — изменяемая ссылка: завтра автор пересоберёт 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.
Запустим 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. Но имя удобнее.
Приложение в контейнере пишет журнал в стандартный вывод, 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). 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 мы их уберём.
Два параметра, которые почти всегда пишут вместе как -it, делают разное:
-i (interactive) — держать стандартный ввод открытым и передавать его процессу;-t (tty) — выделить процессу терминал: появятся приглашение, цвета, работа клавиш со стрелками.$ 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 откажется запускать контейнер — последняя команда выше.
Код завершения главного процесса — код контейнера. Его видно в 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.
Настройки передаются контейнеру переменными окружения, параметр -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: контейнер переживёт перезагрузку хоста и падение приложения, но не воскреснет, если вы остановили его сами.
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).
Образ нельзя удалить, пока из него создан хоть один контейнер, даже остановленный:
$ 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 — с осторожностью