Контейнер - это не маленькая виртуалка, а обычный процесс хоста, которому ядро Linux подсунуло урезанный взгляд на систему через namespaces и cgroups. Docker сам ничего из этого не изобретает, он лишь удобная обёртка над ядром плюс формат упаковки файловой системы (образ). В этом уроке разберём, как устроена пара демон-клиент, как из Dockerfile собирается образ слой за слоем, почему кэш сборки то спасает минуты, то портит вам жизнь, как теги и реестры превращают локальный образ в артефакт для деплоя, и чем podman отличается от docker, не требуя демона вообще.

Как это работает
Классический Docker состоит из двух частей. Демон dockerd работает как root-процесс и держит сокет /var/run/docker.sock, а тонкий клиент docker через этот сокет отдаёт ему команды по REST API. Всё реальное (запуск контейнеров, тяги образов) делает демон, поэтому доступ к сокету равен правам root - это первая мысль про безопасность. В версии 29 демон по умолчанию хранит образы уже не в своём legacy-хранилище, а в containerd, что даёт честную поддержку нескольких архитектур в одном теге.
Образ - это стопка слоёв, доступных только на чтение. Каждый слой это разница в файловой системе относительно предыдущего. Когда вы запускаете контейнер, сверху кладётся тонкий слой для записи, и все изменения уходят туда (механизм copy-on-write, обычно через overlayfs). Удалили контейнер - этот верхний слой исчез, образ остался нетронутым. Отсюда правило: данные, которые должны пережить контейнер, живут в томах, а не в слое записи.
Dockerfile - это рецепт сборки. Каждая инструкция, меняющая файловую систему (RUN, COPY, ADD), порождает новый слой и кэшируется. При повторной сборке демон идёт сверху вниз и переиспользует слой, пока совпадают и инструкция, и её входные данные. Первое же изменение ломает кэш для этой строки и всех нижних. Поэтому редко меняющееся (установку пакетов) ставят выше, а часто меняющееся (свой код) - ниже. С 2018 года движок сборки по умолчанию BuildKit: он строит граф зависимостей, тянет независимые шаги параллельно и не пересобирает то, что не нужно.
Реестр - это сервер хранения образов (Docker Hub, GitHub Container Registry, Harbor, ваш приватный). Полное имя образа складывается из реестра, репозитория и тега: registry.example.com/team/app:1.4. Нет реестра в имени - подставляется Docker Hub, нет тега - подставляется latest. push отправляет слои в реестр, pull тянет их обратно, причём оба переносят только недостающие слои.
Команды и примеры
Базовый жизненный цикл контейнера:
Код: Выделить всё
docker run -d --name web -p 8080:80 nginx:1.27 # запустить в фоне, проброс порта
docker ps # живые контейнеры
docker ps -a # включая остановленные
docker exec -it web sh # войти внутрь работающего
docker logs -f web # смотреть stdout/stderr
docker stop web && docker rm web # корректно погасить и удалить
Минимальный Dockerfile для приложения:
Код: Выделить всё
FROM python:3.13-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8000
ENTRYPOINT ["gunicorn"]
CMD ["app:app", "-b", "0.0.0.0:8000"]
Сборка, теги и реестр:
Код: Выделить всё
docker build -t myapp:1.4 . # собрать и пометить
docker tag myapp:1.4 ghcr.io/me/myapp:1.4 # добавить второе имя
docker login ghcr.io
docker push ghcr.io/me/myapp:1.4 # отправить в реестр
docker pull ghcr.io/me/myapp:1.4 # притянуть на другой машине
docker history myapp:1.4 # посмотреть слои и их размер
Код: Выделить всё
apt-get install ca-certificates curl
install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
apt-get update
apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin
Код: Выделить всё
dnf install dnf-plugins-core
dnf config-manager --add-repo https://download.docker.com/linux/fedora/docker-ce.repo
dnf install docker-ce docker-ce-cli containerd.io docker-buildx-plugin
systemctl enable --now docker
Частые грабли
- latest - это не самая новая версия, а буквально тег по имени latest. Никто не двигает его автоматически. В продакшене пиннуйте конкретный тег или digest (image@sha256:...), иначе сборка перестанет быть воспроизводимой.
- COPY . . без файла .dockerignore затаскивает в контекст сборки .git, node_modules и секреты, раздувает образ и ломает кэш на каждом коммите.
- Каждый RUN это слой. Цепочка из пяти RUN с apt-get оставит мусор и кэш пакетов в промежуточных слоях. Объединяйте через && и чистите в той же строке (rm -rf /var/lib/apt/lists/*).
- Shell-форма CMD ping host запускает процесс под /bin/sh -c, и ваше приложение становится PID 2. SIGTERM уходит в шелл, а не в приложение, и docker stop вырождается в SIGKILL через 10 секунд. Берите exec-форму.
- ENTRYPOINT и CMD путают. ENTRYPOINT - что запускать всегда, CMD - аргументы по умолчанию. Если задан только CMD, его легко переопределить; если ENTRYPOINT в shell-форме, он молча проглотит CMD.
- docker rm не удаляет образ, удаляет контейнер. Для образов docker rmi, для уборки висящего - docker image prune. Болтающиеся <none>:<none> образы это потерявшие тег слои, а не битьё.
- В rootless podman порты ниже 1024 по умолчанию не пробрасываются обычным пользователем - правьте net.ipv4.ip_unprivileged_port_start или маппьте на высокий порт.
- Создайте каталог проекта, положите туда простой Dockerfile на базе alpine с RUN apk add --no-cache curl и CMD ["sleep", "3600"] в exec-форме.
- Соберите образ: docker build -t lab:v1 . Запустите docker history lab:v1 и запишите, сколько слоёв получилось и их размеры.
- Запустите контейнер в фоне, зайдите внутрь через docker exec -it и убедитесь, что curl установлен. Выйдите, не останавливая контейнер.
- Поменяйте только CMD (например аргумент sleep 60), пересоберите и сравните по docker history, какие слои взялись из кэша, а какие пересобраны.
- Перетегируйте образ под локальный реестр: запустите docker run -d -p 5000:5000 registry:2, сделайте docker tag и docker push localhost:5000/lab:v1.
- Удалите локальный образ через docker rmi, затем docker pull localhost:5000/lab:v1 - убедитесь, что он тянется из реестра.
- Повторите сборку этого же Dockerfile командой podman build и запустите podman run от обычного пользователя без sudo. Сравните, под каким UID на хосте крутится процесс (ps aux), с docker.
- Чем слой записи запущенного контейнера отличается от слоёв образа и что с ним происходит при docker rm?
- Почему COPY requirements.txt отдельной строкой до COPY всего кода ускоряет повторные сборки?
- В чём разница между ENTRYPOINT и CMD и почему для них рекомендуют exec-форму, а не shell-форму?
- Из каких частей складывается полное имя образа и что подставляется, если опустить реестр и тег?
- Что именно переносят по сети docker push и docker pull, и почему повторная тяга обычно быстрее первой?
- За счёт чего podman работает без постоянного демона и в чём практическое следствие этого для безопасности по сравнению с docker.sock?