← назад к разделу

DOCKER Фаза 14. Контейнеры: Docker

Обязательно

Что такое Docker и контейнеры

Java · Go · Node · Python

Что такое Docker контейнер и образ, чем контейнер отличается от виртуальной машины, где проходит предел изоляции и зачем всё это бэкендеру в ежедневной работе.

1. Контейнер - изолированный процесс Linux

1 / 9

Контейнер не эмулирует компьютер. Это обычный процесс хоста, которому ядро показывает урезанный мир и выдаёт ограниченные ресурсы.

  • namespaces отвечают за видимость: файловая система, процессы, сеть, идентификаторы.
  • cgroups отвечают за количество: память, процессор, ввод-вывод.
  • Общее ядро не заменяет безопасность: отдельно настраивают non-root и лимиты.
Обычный процесс на хосте: границ нет ядро и ресурсы хоста PID 410 чужие процессы тоже видны /etc и /varфайлы хоста вся память и все CPU Без namespaces и cgroups контейнер ничем не отличается от процесса хоста Тот же процесс, но мир вокруг него урезан общее ядро хоста namespace PID 1 своя ФС свои PID, сеть, точки монтирования cgroupsне видит, а ограничивает 512 MiB из 2 GiB · 1 CPU из 2 Изоляция = урезанная видимость + явные лимиты ресурсов

2. Виртуальная машина против контейнера

2 / 9

Разница не в слове «изоляция», а в том, сколько слоёв стоит между приложением и железом.

  • ВМ поднимает гостевую ОС и своё ядро: изоляция сильнее, но старт идёт минуты и весит гигабайты.
  • Контейнер запускает только процесс: старт секунды, образ обычно мегабайты, но ядро общее.
  • Поэтому контейнер не называют «маленькой ВМ»: у них разные границы безопасности.
ВМ: каждая несёт свою ОС и своё ядро приложениеприложениеприложение гостевая ОСсвоё ядрогостевая ОСсвоё ядрогостевая ОСсвоё ядро гипервизор: эмулирует железо для каждой ВМ ядро хоста и железо Три гостевых ядра: сильная изоляция ценой памяти и времени старта Контейнеры: гостевых ОС нет, ядро одно приложениерантайм и ФСnamespace приложениерантайм и ФСnamespace приложениерантайм и ФСnamespace одно ядро хостаоно создаёт namespaces и применяет cgroups железо без гипервизора между процессами Легко и быстро, но граница проходит на уровне процессов

3. Образ - шаблон, контейнер - запущенный экземпляр

3 / 9

Образ хранит приложение, рантайм, библиотеки и настройки как неизменяемый набор слоёв. Контейнер начинается с того же набора, но получает свой записываемый слой.

  • Один образ можно запустить десять раз: начальное содержимое будет одинаковым.
  • Изменения контейнеров не переписывают образ и не протекают друг в друга.
  • Данные, которые должны пережить удаление контейнера, выносят в том, а не в записываемый слой.
Образ myapp:1.0 - неизменяемый шаблон приложение и конфигурация библиотеки и рантайм базовая файловая система слои только для чтения: версия зафиксирована тегом 1.0 Внутри нет работающего процесса - это пакет для будущего запуска Один образ, три независимых контейнера образ myapp:1.0 записываемый слой Aзаписываемый слой Bзаписываемый слой C слои образа R/Oслои образа R/Oслои образа R/O A, B и C меняются независимо, образ остаётся прежним Для базы и других ценных данных нужен внешний том

4. Docker решает «на моей машине работает»

4 / 9

Приложение перестаёт зависеть от версии рантайма и библиотек на конкретной машине: они едут внутри образа. Но настройки и секреты остаются снаружи.

  • Dev, staging и production запускают один тег, например myapp:1.0.
  • Адрес базы, пароль и размер памяти передают через окружение, ConfigMap или Secret.
  • Один образ не отменяет архитектуру процессора: её проверяют отдельно.
Один исходник, два разных окружения ноутбук разработчикаproduction приложениеприложение рантайм v1рантайм v2 библиотека A v1библиотека A v2 тот же код непредсказуемая ошибкау меня работает Один образ едет по стендам без пересборки myapp:1.0 devstagingproduction тот же образтот же образтот же образ снаружи: DB_URL, SECRET, лимиты памяти и CPU

5. От Dockerfile до работающего контейнера

5 / 9

Dockerfile - воспроизводимый рецепт образа. Он фиксирует базу, файлы, зависимости и команду запуска, поэтому одинаково собирается на разных машинах.

  • docker build -t myapp:1.0 . создаёт образ из Dockerfile.
  • docker run myapp:1.0 создаёт и запускает контейнер, docker ps показывает его.
  • docker stop останавливает процесс, docker rm удаляет остановленный контейнер.
docker build: текст превращается в слои образа Dockerfile FROM runtimeWORKDIR /appCOPY appRUN installENTRYPOINT["./app"] build слой приложения слой зависимостей базовый слой myapp:1.0 Повторная сборка по тому же рецепту даёт то же содержимое Жизненный цикл контейнера образ run running stop stopped rm нет docker psCONTAINER ID IMAGE STATUSa81f myapp:1.0 Up 2 minutes rm удаляет контейнер, но не образ и не тома с данными

6. Testcontainers: настоящая база вместо заменителя

6 / 9

Библиотека поднимает PostgreSQL, Kafka, Redis или другой образ прямо из теста. Проверка идёт против настоящего поведения, а не совместимого подмножества H2.

  • Свободный порт выбирается автоматически, адрес передаётся тестовому клиенту.
  • После набора тестов контейнер удаляется, а Ryuk чистит остатки даже после аварии.
  • Reuse ускоряет локальные запуски, в CI оставляют чистые контейнеры и свой префикс реестра в testcontainers.properties.
Тест сам поднимает настоящую базу тестовый класс@BeforeAll start PostgreSQLпорт 49153 JDBC URL тесты зелёные jsonb и планы как в production После тестов: чистка, локально - ускорение тест завершёнили процесс упал Ryuk нет контейнеров локально: withReuse(true)база остаётся между прогонами CI: чистый стартregistry.corp/postgres:17 Скорость локально не должна превращаться в общее состояние между CI-тестами

7. Очистка диска без потери базы

7 / 9

В `/var/lib/docker` лежат не только образы: контейнеры, тома и кэш сборки тоже занимают место. На macOS и Windows всё это находится внутри виртуальной машины Docker.

  • docker system df показывает категории и долю reclaimable.
  • docker image prune убирает ненужные образы, docker builder prune - кэш.
  • system prune -a --volumes удаляет и тома с базами: без полной уверенности этот флаг не добавляют.
docker system df: из чего состоят 26 GB образы 8 GBконтейнеры 2 GBтома 6 GBкэш 10 GB старые версииостановленныебазы и данныешаги сборки RECLAIMABLE: можно освободить, но не всё безопасно В `/var/lib/docker` или в диске виртуальной машины Docker Desktop Два исхода очистки без --volumesс --volumes image prunebuilder pruneкэша больше нет база цела image prunebuilder prunevolume prune данные базы потеряны кэш удалён, данные сохранены

8. OCI: почему образ работает и без Docker

8 / 9

OCI стандартизирует формат образа, распространение и запуск. Docker реализует эти стандарты, но не владеет ими.

  • CLI Docker обращается к containerd, а runc создаёт процесс с namespaces и cgroups.
  • Kubernetes 1.24+ работает с containerd напрямую, образ менять не нужно.
  • Podman запускает контейнеры, Buildah и Kaniko собирают образы, Nerdctl обращается к containerd.
Что происходит после docker run docker CLIкоманда containerdжизненный цикл runcсоздаёт PID ядро Linux: namespaces + cgroups Docker - удобный интерфейс и сборщик, а не сам контейнерный процесс Один OCI-образ, разные инструменты OCI-образ Kubernetes 1.24+ Podman Buildah / Kaniko Nerdctl containerd / runc Меняется инструмент, формат образа и поведение остаются прежними

9. Образ зависит от архитектуры процессора

9 / 9

Одинаковый рантайм не гарантирует запуск: бинарные инструкции ARM64 и AMD64 различаются. Это отдельная часть окружения, которую Dockerfile не решает сам.

  • Образ ARM64 на AMD64 падает с exec format error, а эмуляция может работать в разы медленнее.
  • docker buildx build --platform linux/amd64,linux/arm64 собирает два варианта.
  • Manifest list связывает их под одним тегом, и Docker выбирает вариант под свою архитектуру.
Один тег, несовместимый процессор ноутбук Apple SiliconARM64 myapp:1.0 тот же образ серверAMD64 exec format error Если эмуляция всё же запускается, её цена - заметное замедление buildx: две сборки под одним тегом manifest: myapp:1.0 образ AMD64 образ ARM64 сервер AMD64ноутбук ARM64 Docker выбирает подходящий слой автоматически

На ноутбуке сервис работает, на сервере падает. Docker закрывает эту дыру: приложение упаковывают вместе со всем окружением и запускают одинаково на любой машине.

Ниже — два способа запустить три копии одного приложения на одном сервере: через виртуальные машины и через контейнеры.

Задача: 3 копии myapp:1.0 на одном серверевиртуальные машиныконтейнерыжелезожелезо ядро хостагипервизорэмулирует железо для каждой ВМ приложениерантаймгостевая ОСсвоё ядроприложениерантаймгостевая ОСсвоё ядроприложениерантаймгостевая ОСсвоё ядро 3 гостевых ядра + хостовоеразмер: гигабайтыстарт: минуты ядро хоста — одно на всех namespaces ядра: что контейнер видитcgroups ядра: сколько CPU и памяти приложениерантаймсвоя ФСприложениерантаймсвоя ФСприложениерантаймсвоя ФС одно ядро — хостовоеразмер: десятки-сотни МБстарт: секунды Плата за лёгкость — ядро общееизоляция контейнера на уровне процессов, у ВМ — аппаратная

Три копии на виртуальных машинах — это три гостевых ядра поверх хостового; три контейнера — три процесса на одном ядре хоста. Отсюда и секунды вместо минут, и предел изоляции.

Проблема: «на моей машине работает»

Представьте: вы написали сервис, протестировали локально — всё работает. Отдаёте коллеге — падает. Деплоите на сервер — снова проблема. Причина почти всегда одна: окружение отличается.

У вас одна версия языка, у коллеги — другая. На сервере другая версия библиотеки. Переменная окружения называется чуть иначе. Такие расхождения копятся и превращаются в непредсказуемые ошибки, которые сложно воспроизвести.

Раньше эту проблему решали виртуальными машинами: поднять отдельную ОС, настроить её вручную, передать образ диска на несколько гигабайт. Это работало, но было тяжело и медленно.

Docker предложил более лёгкий подход — контейнеры.

Что такое контейнер

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

Изоляцию обеспечивают два механизма ядра Linux, и отвечают они за разное. Чтобы процесс не видел чужих процессов, сети и файлов, ядро выдаёт ему отдельные пространства имён — namespaces. Чтобы он не съел всю память и все ядра машины, его сажают в контрольную группу — cgroups (control groups) с лимитами на CPU и память.

Про cgroups есть важная оговорка: сам по себе Docker ничего не ограничивает. Механизм готов, но пока вы не попросили лимит флагами --memory и --cpus, контейнер спокойно съест всю память машины — и утащит за собой соседей.

Это принципиально отличает контейнер от виртуальной машины.

Контейнер vs виртуальная машина

Виртуальная машинаКонтейнер
Ядро ОССвоё (гостевое)Общее с хостом
ЗапускМинутыСекунды / доли секунды
РазмерГигабайтыДесятки–сотни мегабайт
ИзоляцияПолная (аппаратный уровень)На уровне процессов

Виртуальная машина эмулирует целый компьютер: у неё своё ядро, свои драйверы, своя ОС. Это даёт максимальную изоляцию, но требует значительных ресурсов.

Контейнер использует ядро хоста и добавляет изоляцию поверх него. По умолчанию он не знает о соседних контейнерах и не видит файлов хоста — пока вы сами не откроете ему нужный каталог (как это делается, разбираем в статье про тома). Зато процессы в нём запускаются почти без накладных расходов.

Ещё одна оговорка — про «общее ядро». Так устроен Linux: там контейнер и правда процесс вашей же системы. На macOS и Windows Docker держит рядом небольшую виртуальную машину с Linux и запускает контейнеры внутри неё. «Хост» для контейнера — эта машина, а не ваш ноутбук, и память с процессором контейнер берёт из того, что вы ей выделили в настройках Docker Desktop.

Виртуальная машина нужна, когда требуется другое ядро или другая ОС либо жёсткая изоляция от соседей; для запуска своих сервисов бэкендеру хватает контейнеров.

Предел изоляции: общее ядро и root по умолчанию

Фраза «изоляция на уровне процессов» звучит успокаивающе, а практический вывод из неё неприятный, и лучше узнать его сразу.

Ядро одно на всех. Контейнеры не приносят своё ядро — они пользуются ядром хоста. Значит, уязвимость в ядре — это уязвимость всех контейнеров сразу, и выход из контейнера («побег») через такую уязвимость возможен. Виртуальная машина в этом месте честнее: у неё своё ядро, и цена побега на порядок выше. Отсюда правило: контейнеры разделяют ваши нагрузки друг от друга, а не чужой недоверенный код от вашей машины. Для запуска непроверенного кода берут виртуальные машины или специальные изолированные среды.

Процесс внутри по умолчанию работает от root. Это тот же root, что и на хосте: пространства имён пользователей по умолчанию не включены, и uid 0 внутри означает uid 0 снаружи. Пока контейнер ничего не монтирует с хоста, беды нет; стоит смонтировать каталог хоста — и процесс внутри правит файлы хоста с правами суперпользователя. Поэтому в образах заводят обычного пользователя и переключаются на него (USER), а про остальные способы ограничить запуск говорит статья про промышленные образы.

Что ещё общее. Ядро — не единственный общий ресурс: без явных ограничений контейнер видит всю память и все процессоры машины и может выесть их целиком, уронив соседей. Лимиты задают флагами при запуске, и именно на этом спотыкаются рантаймы, о чём отдельная статья фазы про ваш язык в контейнере: JVM в контейнере, рантайм Go, Node, Python.

Вывод в одну строку: контейнер — это удобная упаковка и разумная изоляция, но не граница безопасности того уровня, что даёт виртуальная машина.

Образ и контейнер: класс и объект

Прежде чем запустить контейнер, нужен образ (image). Образ — это неизменяемый слепок файловой системы: все файлы приложения, зависимости, конфигурация, нужная версия языка.

Соотношение простое:

  • Образ — шаблон (как класс в ООП). Он неизменен, хранится на диске или в реестре.
  • Контейнер — запущенный экземпляр образа (как объект). Их может быть несколько из одного образа.

Один образ myapp:1.0 можно запустить как один контейнер на ноутбуке разработчика, второй — на тестовом сервере, третий — в production. Каждый получает идентичное окружение.

Где это помогает бэкендеру

Одинаковое окружение везде. Dev, staging, production работают на одном образе. «На моей машине работает» перестаёт быть аргументом.

Быстрый старт и остановка. Контейнер с сервисом стартует за секунды. Это удобно как для разработки, так и для горизонтального масштабирования под нагрузкой.

Изоляция зависимостей. Два приложения требуют разные версии одной библиотеки — каждое запускается в своём контейнере без конфликтов.

Воспроизводимая сборка. Образ собирается по инструкции (Dockerfile) и воспроизводится одинаково на любой машине. Это основа надёжного CI/CD.

Первый взгляд на базовые команды

Ниже — обзорные примеры, как Docker выглядит на практике. Детали каждой команды разобраны в статье про запуск контейнеров.

# Запустить контейнер из образа eclipse-temurin:21-jre
docker run eclipse-temurin:21-jre java -version

# Посмотреть запущенные контейнеры
docker ps

# Остановить контейнер по его ID или имени
docker stop <container_id>

# Удалить остановленный контейнер
docker rm <container_id>
# Запустить контейнер из образа golang:1.23
docker run golang:1.23 go version

# Посмотреть запущенные контейнеры
docker ps

# Остановить контейнер по его ID или имени
docker stop <container_id>

# Удалить остановленный контейнер
docker rm <container_id>
# Запустить контейнер из образа node:22-slim
docker run node:22-slim node --version

# Посмотреть запущенные контейнеры
docker ps

# Остановить контейнер по его ID или имени
docker stop <container_id>

# Удалить остановленный контейнер
docker rm <container_id>
# Запустить контейнер из образа python:3.12-slim
docker run python:3.12-slim python --version

# Посмотреть запущенные контейнеры
docker ps

# Остановить контейнер по его ID или имени
docker stop <container_id>

# Удалить остановленный контейнер
docker rm <container_id>

docker run скачивает образ из реестра (если его нет локально), создаёт контейнер и запускает указанную команду. docker ps показывает, что сейчас работает.

Реестр по умолчанию — Docker Hub. В браузере на него смотрят через hub.docker.com, а качает Docker с docker.io — это и есть имя реестра, которое прописывают в настройках, когда подменяют его на внутреннее зеркало. Там хранятся официальные образы: postgres, nginx, golang, node, python, eclipse-temurin и сотни других.

Теперь короткая формула читается целиком: контейнер = изолированный процесс + своя файловая система.

Но есть одна оговорка, на которой новичок спотыкается первым, и обещание «на моей машине работает» ломается именно о неё: у образа есть архитектура процессора. Ноутбук на Apple Silicon — это arm64, обычный сервер — amd64, и образ, собранный на одном, на другом не запустится или запустится через медленную эмуляцию. Проявляется это по-разному: exec format error при запуске, «непонятно почему в десять раз медленнее», или сборка, которая проходит локально и падает в конвейере.

Официальные образы обычно собраны под обе архитектуры, и Docker выбирает нужную сам; проблема начинается с вашими собственными образами. Решается это сборкой сразу под две архитектуры (docker buildx build --platform linux/amd64,linux/arm64) или явным указанием целевой платформы — разбор в статье про реестры и конвейер.

Где всё это лежит и почему кончается место

Через неделю работы с Docker появляется бытовой вопрос: на диске стало на двадцать гигабайт меньше, а ничего вроде не скачивали.

Всё, что Docker создаёт, живёт в одном каталоге — на Linux это /var/lib/docker. Внутри: слои образов (в подкаталоге overlay2 — так называется файловая система, которая складывает слои друг на друга), записываемые слои остановленных контейнеров, тома с данными и кэш сборки. На macOS и Windows этого каталога на хосте нет: Docker там работает внутри маленькой виртуальной машины с Linux, и всё лежит в её диске — одном большом файле, который только растёт.

Посмотреть, куда ушло место, можно одной командой:

docker system df

Она печатает четыре строки — образы, контейнеры, тома, кэш сборки — и в каждой показывает, сколько из этого можно освободить (RECLAIMABLE). Типичная картина после месяца работы: десяток забытых образов старых версий, кэш сборки на несколько гигабайт и тома от контейнеров, которых уже нет.

Убирают лишнее так:

docker image prune        # образы без тегов (промежуточные и перезаписанные)
docker builder prune      # кэш сборки
docker container prune    # остановленные контейнеры
docker volume ls -f dangling=true   # сначала посмотреть, какие тома ничьи

Одна команда стоит особняком: docker system prune -a --volumes вычищает всё сразу, включая тома. Она освобождает больше всех и она же уносит данные баз, поднятых локально: том с базой не отличается от забытого тома. Пока не уверены — не добавляйте --volumes.

Docker — не единственный способ запускать контейнеры

Для первого знакомства «контейнеры = Docker» — рабочее упрощение, но дальше по программе его придётся разучивать, поэтому лучше сразу знать расстановку.

Контейнеры описаны стандартами OCI (Open Container Initiative): формат образа, формат распространения и требования к тому, что запускает контейнер. Docker — одна из реализаций этих стандартов, а не их владелец. Отсюда практическое следствие: образ, собранный Docker, запустится где угодно, а собрать его можно и без Docker.

Внутри самого Docker работа поделена. Есть containerd — служба, которая на самом деле управляет жизненным циклом контейнеров, и runc — программа, которая непосредственно создаёт процесс с нужными пространствами имён и ограничениями. docker — это удобный интерфейс поверх них плюс сборщик образов.

Почему это важно знать. Kubernetes раньше умел работать через Docker, а начиная с версии 1.24 обращается прямо к containerd (или другой совместимой среде выполнения). Слой совместимости убрали — и это не «Kubernetes отказался от контейнеров Docker», а «Kubernetes перестал ходить в них через лишнего посредника». Ваши образы при этом продолжают работать без изменений, потому что они соответствуют стандарту.

Рядом есть и другие инструменты того же назначения: podman (запускает контейнеры без постоянной службы и умеет работать без прав суперпользователя), buildah и kaniko (собирают образы без Docker — это удобно внутри самого кластера), nerdctl (командная строка прямо к containerd). Для читателя этой фазы вывод простой: изучайте команды Docker, а помните, что вы изучаете стандарт, у которого несколько реализаций.

Дополнительно: при первом чтении можно пропустить

Глубже: тот же Docker из тестов: Testcontainersрасширенное

Первое, для чего бэкендер пользуется Docker каждый день, это не выкат, а тесты. Интеграционный тест против встроенной базы вроде H2 или SQLite проходит там, где PostgreSQL упадёт на первом же jsonb, и наоборот; проверять нужно на настоящей базе, и Testcontainers поднимает её из теста тем же Docker, что описан выше.

@SpringBootTest
@Testcontainers
class OrderRepositoryTest {

    @Container
    @ServiceConnection
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:17");

    @Test
    void savesOrder() { ... }
}
func TestSavesOrder(t *testing.T) {
	ctx := context.Background()
	pg, err := postgres.Run(ctx, "postgres:17", postgres.BasicWaitStrategies())
	if err != nil {
		t.Fatal(err)
	}
	t.Cleanup(func() { pg.Terminate(ctx) })

	dsn, _ := pg.ConnectionString(ctx, "sslmode=disable")
	...
}
describe('OrderRepository', () => {
  let postgres: StartedPostgreSqlContainer;

  beforeAll(async () => {
    postgres = await new PostgreSqlContainer('postgres:17').start();
  }, 60_000);
  afterAll(() => postgres.stop());

  it('saves order', async () => { ... });
});
@pytest.fixture(scope="session")
def postgres():
    with PostgresContainer("postgres:17") as pg:
        yield pg


def test_saves_order(postgres):
    dsn = postgres.get_connection_url()
    ...

Библиотека находит Docker на машине, скачивает образ, запускает контейнер на случайном свободном порту, ждёт, пока база ответит, и подставляет адрес в приложение (в Spring через @ServiceConnection, в Go, Node и Python — строкой подключения из объекта контейнера); после тестов контейнер удаляется. То же для Kafka, RabbitMQ, Redis, Elasticsearch, Keycloak и любого образа через GenericContainer. Один контейнер на класс тестов (static) стартует за секунды, на каждый тест дороже без нужды.

Три вещи, которые узнают из ошибок. Рядом со своими контейнерами Testcontainers запускает служебный контейнер Ryuk: он следит за тестовым процессом и удаляет всё, что тот оставил, если тестовый процесс убили; в CI, где Docker урезан, его отключают переменной TESTCONTAINERS_RYUK_DISABLED=true и чистят сами.

Повторное использование: withReuse(true) в коде и testcontainers.reuse.enable=true в ~/.testcontainers.properties оставляют контейнер жить между запусками, и локально тесты идут в разы быстрее; в CI это выключено. И реестр: образы тянутся из Docker Hub, а он бывает недоступен, о чём статья про реестры; префикс своего реестра задают в testcontainers.properties строкой hub.image.name.prefix, и тогда postgres:17 превращается в registry.corp/dockerhub/postgres:17 без правки тестов.

В CI Testcontainers нужен доступ к Docker: либо агент сам работает на машине с Docker, либо ему прокидывают /var/run/docker.sock (что равносильно правам root на узле, о чём статья про безопасность образов), либо используют управляемый сервис контейнеров для тестов. Это первый вопрос при заведении конвейера, а не последний.

Что почитать дальше