Почему «у меня работает» перестаёт быть отговоркой и что на самом деле запускает docker run
docker rundocker — фактически rootПриложению мало собственного кода. Сайту на Python нужен интерпретатор определённой версии, десяток библиотек, системные пакеты вроде libpq для PostgreSQL, файлы настроек и переменные окружения. На ноутбуке разработчика всё это когда-то поставили руками, на сервере — по-другому, на втором сервере — третьим способом. Отсюда «у меня работает»: код одинаковый, окружение разное.
Контейнер упаковывает приложение вместе с окружением: файлы, библиотеки, настройки — всё, кроме ядра операционной системы. Упаковка называется образом (image). Запущенный из образа процесс называется контейнером. Один и тот же образ одинаково запускается на ноутбуке, на тестовом и на боевом сервере — меняются только настройки, переданные при запуске.
| Задача | Без контейнеров | С контейнерами |
|---|---|---|
| Поставить приложение на новый сервер | Инструкция на страницу, которую никто не обновляет | docker run образ или docker compose up |
| Два приложения хотят разные версии Python | Конфликт пакетов или ручные сборки в /opt | У каждого свой образ со своей версией |
| Откатить неудачное обновление | Вспоминать, что меняли | Запустить предыдущий образ |
| Поднять базу данных для тестов | Ставить СУБД на рабочую машину | Контейнер на время тестов, потом удалить |
Контейнер не решает всё. Данные (база, загруженные файлы) живут вне образа — об этом модуль 04. Настройки и пароли тоже передаются снаружи — модули 06 и 08. И контейнер не делает приложение безопаснее сам по себе: он изолирует, но не так строго, как виртуальная машина.
Виртуальная машина эмулирует компьютер целиком: у неё своё ядро, своя загрузка, своя выделенная память. Контейнер — обычный процесс Linux на ядре хоста, которому ядро «показывает» отдельную картину мира: свой список процессов, свою файловую систему, свою сеть, своё имя машины.
Проверим. Хост — Debian 13, контейнер — из образа Alpine Linux:
$ grep PRETTY /etc/os-release; uname -r
PRETTY_NAME="Debian GNU/Linux 13 (trixie)"
6.12.95+deb13-amd64
$ docker run --rm alpine:3.22 sh -c "grep PRETTY /etc/os-release; uname -r"
PRETTY_NAME="Alpine Linux v3.22"
6.12.95+deb13-amd64
Файлы в контейнере свои — /etc/os-release от Alpine. А ядро одно и то же — ядро Debian. Поэтому в контейнере на Linux нельзя запустить программу для Windows, а Docker Desktop на Windows и macOS запускает контейнеры внутри скрытой виртуальной машины с Linux.
Внутри контейнер видит только свои процессы. Команда ps — единственный процесс с PID 1, имя машины — случайный идентификатор контейнера:
$ docker run --rm alpine:3.22 ps
PID USER TIME COMMAND
1 root 0:00 ps
$ docker run --rm alpine:3.22 hostname
1e6d689e424f
Снаружи тот же процесс — обычная строка в списке процессов хоста, со своим PID хоста. Запустим контейнер в фоне и найдём его:
$ docker run -d --name sleeper alpine:3.22 sleep 300
4fc094f8a96fc81f64c84fb5e9e83585122773f6d81726d1cf95eae82024725a
$ docker inspect -f "{{.State.Pid}}" sleeper
1629933
$ ps -o pid,user,cmd -p $(docker inspect -f "{{.State.Pid}}" sleeper)
PID USER CMD
1629933 root sleep 300
$ docker top sleeper
UID PID PPID C STIME TTY TIME CMD
root 1629933 1629909 12 20:08 ? 00:00:00 sleep 300
$ docker rm -f sleeper
sleeper
PID 1 внутри и шестизначный PID снаружи — один и тот же процесс sleep 300. Никакой «машины» вокруг него нет.
| Виртуальная машина | Контейнер | |
|---|---|---|
| Ядро | Своё | Общее с хостом |
| Запуск | Десятки секунд: загрузка ОС | Доли секунды: запуск процесса |
| Размер | Гигабайты: целая ОС | Мегабайты: только нужные файлы |
| Изоляция | Сильная: граница — гипервизор | Слабее: граница — ядро, уязвимость ядра опасна для всех контейнеров |
| Другая ОС | Любая, в том числе Windows | Только Linux на Linux |
На практике их сочетают: облачный сервер — виртуальная машина, на нём — контейнеры приложений.
Docker не изобретал контейнеры — он собрал в удобный инструмент механизмы ядра Linux, которые существовали до него:
| Механизм | Что делает | Что видно в контейнере |
|---|---|---|
| Пространства имён (namespaces) | Дают процессу отдельный вид на ресурс: pid — процессы, net — сеть, mnt — точки монтирования, uts — имя машины, ipc, user | Свой PID 1, свой hostname, свой сетевой интерфейс eth0 |
| Контрольные группы (cgroups) | Ограничивают и считают ресурсы группы процессов: память, процессор, число процессов | Контейнер с --memory 256m не съест память хоста (модуль 07) |
| Слоёная файловая система (overlayfs) | Складывает слои образа, только для чтения, и сверху — слой для записи | Десять контейнеров из одного образа не копируют его десять раз |
| Capabilities, seccomp, AppArmor | Отнимают у root в контейнере часть прав ядра | root в контейнере не может, например, загрузить модуль ядра |
Команда docker — только клиент. Работу делает служба, и между ними несколько слоёв:
docker (клиент) ──REST API через /run/docker.sock──▶ dockerd (служба Docker)
│ образы, сети, тома, сборка
▼
containerd (жизненный цикл контейнеров)
│
▼
runc (создаёт процесс в namespaces и cgroups)
docker version показывает обе стороны — клиент и сервер — и версии нижних слоёв:
$ docker version
Client:
Version: 26.1.5+dfsg1
API version: 1.45
Go version: go1.24.4
Git commit: a72d7cd
Built: Thu Aug 13 05:35:47 2026
OS/Arch: linux/amd64
Context: default
Server:
Engine:
Version: 26.1.5+dfsg1
API version: 1.45 (minimum version 1.24)
Go version: go1.24.4
Git commit: 411e817
Built: Thu Aug 13 05:35:47 2026
OS/Arch: linux/amd64
Experimental: false
containerd:
Version: 1.7.24~ds1
GitCommit: 1.7.24~ds1-6+deb13u1
runc:
Version: 1.1.15+ds1
GitCommit: 1.1.15+ds1-2+b4
docker-init:
Version: 0.19.0
GitCommit:
Слово «Docker» обозначает несколько вещей, их полезно различать:
dockerd с клиентом, бесплатна и с открытым кодом. Ей посвящён этот курс.alpine, nginx, postgres.Формат образов и порядок запуска стандартизованы (спецификации OCI), поэтому образ, собранный Docker, запускают Podman, containerd в Kubernetes и другие среды.
Есть два пути. Первый — пакеты самого Debian: версия старше, зато обновления безопасности приходят вместе с остальной системой через apt upgrade. Для учёбы и большинства серверов этого достаточно:
$ sudo apt update
$ sudo apt install docker.io docker-compose docker-buildx
Второй — репозиторий Docker, где свежие версии выходят раньше. Команды — из документации Docker:
$ sudo apt install ca-certificates curl
$ sudo install -m 0755 -d /etc/apt/keyrings
$ sudo curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc
$ echo "deb [signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian trixie stable" | sudo tee /etc/apt/sources.list.d/docker.list
$ sudo apt update
$ sudo apt install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
docker.io и docker-ce конфликтуют. Переходя с одного на другой, сначала удалите первый. Примеры курса сняты на пакетах Debian: Docker 26.1, Compose 2.26 — во всём, что разбирается в курсе, новые версии ведут себя так же.После установки служба уже запущена и включена в автозагрузку. Клиент разговаривает с ней через сокет, доступный root и группе docker:
$ systemctl is-active docker containerd
active
active
$ ls -l /run/docker.sock
srw-rw---- 1 root docker 0 окт 23 19:15 /run/docker.sock
Чтобы не писать sudo перед каждой командой, пользователя добавляют в группу docker и перезаходят в систему (группы применяются при входе):
$ sudo usermod -aG docker $USER
Служба dockerd работает от root и выполняет всё, что попросит клиент. Значит, любой, кто может писать в сокет, может попросить запустить контейнер с корнем хоста внутри. Обычному пользователю каталог /root закрыт, но через контейнер он создаёт там файл:
$ id -un; ls /root
padmin
ls: невозможно открыть каталог '/root': Отказано в доступе
$ docker run --rm -v /:/host alpine:3.22 sh -c "id; ls -ld /host/root; touch /host/root/i-was-here; ls -l /host/root/i-was-here; rm /host/root/i-was-here"
uid=0(root) gid=0(root) groups=0(root),1(bin),2(daemon),3(sys),4(adm),6(disk),10(wheel),11(floppy),20(dialout),26(tape),27(video)
drwx------ 4 root root 4096 Oct 23 17:06 /host/root
-rw-r--r-- 1 root root 0 Oct 23 17:08 /host/root/i-was-here
Так же можно прочитать /etc/shadow, поменять /etc/sudoers или поставить службу. Это не уязвимость, а устройство Docker, и из него следуют правила:
docker только тех, кому вы дали бы sudo./var/run/docker.sock в контейнеры без крайней нужды: взломанный контейнер получит хост.Есть режим rootless, где служба работает от обычного пользователя, и Podman, где службы нет вовсе. У них свои ограничения, им посвящена часть модуля 08.
Образ hello-world существует ради одной проверки: что клиент, служба и доступ к реестру работают.
$ docker run hello-world
Hello from Docker!
This message shows that your installation appears to be working correctly.
To generate this message, Docker took the following steps:
1. The Docker client contacted the Docker daemon.
2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
(amd64)
3. The Docker daemon created a new container from that image which runs the
executable that produces the output you are currently reading.
4. The Docker daemon streamed that output to the Docker client, which sent it
to your terminal.
To try something more ambitious, you can run an Ubuntu container with:
$ docker run -it ubuntu bash
Share images, automate workflows, and more with a free Docker ID:
https://hub.docker.com/
For more examples and ideas, visit:
https://docs.docker.com/get-started/
Сообщение само перечисляет шаги: клиент обратился к службе; служба не нашла образ локально и скачала его с Docker Hub; создала из образа контейнер и запустила; передала вывод клиенту. При первом запуске перед текстом ещё есть строки Unable to find image 'hello-world:latest' locally и прогресс загрузки — на parser образ уже был скачан.
Программа напечатала текст и завершилась — завершился и контейнер. Но он не исчез: остановленные контейнеры хранятся, пока их не удалят. Образ тоже остался на диске:
$ docker image ls hello-world
REPOSITORY TAG IMAGE ID CREATED SIZE
hello-world latest e2ac70e7319a 7 months ago 10.1kB
$ docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
0c18caf65ae2 hello-world "/hello" 2 seconds ago Exited (0) 2 seconds ago clever_kirch
Имя в колонке NAMES Docker придумал сам, потому что мы не задали --name. Как управлять контейнерами и убирать за собой — в следующем модуле.
По умолчанию образы скачиваются с Docker Hub. Из российских сетей он доступен не всегда: в 2024 году Docker Inc. ограничивала доступ с российских адресов, отдельные провайдеры режут трафик, а у анонимной загрузки есть лимит на число запросов. Если docker pull завис или вернул 403, 429, помогает зеркало — сервер, который отдаёт те же образы.
Зеркало задаётся в /etc/docker/daemon.json:
{
"registry-mirrors": ["https://mirror.gcr.io"]
}
$ sudo systemctl restart docker
$ docker info | grep -A1 "Registry Mirrors"
Registry Mirrors:
https://mirror.gcr.io/
mirror.gcr.io — зеркало Docker Hub от Google; 23.10.2026 оно отдавало образы на учебном сервере курса. Зеркала держат и российские облака и хостеры — адрес смотрите в их документации. Зеркало используется только для образов Docker Hub; образы из других реестров (ghcr.io/…, cr.yandex/…) скачиваются напрямую.
Полезные сведения о службе — версия, драйвер хранения, где лежат данные:
$ docker info | grep -E "Server Version|Storage Driver|Cgroup Version|Docker Root Dir|Kernel Version"
Server Version: 26.1.5+dfsg1
Storage Driver: overlay2
Cgroup Version: 2
Kernel Version: 6.12.95+deb13-amd64
Docker Root Dir: /var/lib/docker
Все образы, контейнеры и тома лежат в /var/lib/docker. На сервере под него стоит заложить место: образы быстро занимают гигабайты.
docker — клиент; работу делают dockerd, containerd и runcapt install docker.io docker-compose docker-buildx или репозиторий Docker — не оба сразуdocker равна root; сокет Docker не открывать в сеть и не отдавать контейнерамregistry-mirrors