На ноутбуке сервис работает, на сервере падает: там другая Java, нет нужной библиотеки, конфиг лежит не там. Docker закрывает эту дыру: приложение упаковывают вместе со всем окружением и запускают одинаково на любой машине.
Ниже — два способа запустить три копии одного приложения на одном сервере: через виртуальные машины и через контейнеры.
Три копии на виртуальных машинах — это три гостевых ядра поверх хостового; три контейнера — три процесса на одном ядре хоста. Отсюда и секунды вместо минут, и предел изоляции.
Проблема: «на моей машине работает»
Представьте: вы написали Spring Boot-приложение, протестировали локально — всё работает. Отдаёте коллеге — падает. Деплоите на сервер — снова проблема. Причина почти всегда одна: окружение отличается.
У вас Java 21, у коллеги — 17. На сервере другая версия библиотеки. Переменная окружения называется чуть иначе. Такие расхождения копятся и превращаются в непредсказуемые ошибки, которые сложно воспроизвести.
Раньше эту проблему решали виртуальными машинами: поднять отдельную ОС, настроить её вручную, передать образ диска на несколько гигабайт. Это работало, но было тяжело и медленно.
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, о чём отдельная статья фазы.
Вывод в одну строку: контейнер — это удобная упаковка и разумная изоляция, но не граница безопасности того уровня, что даёт виртуальная машина.
Образ и контейнер: класс и объект
Прежде чем запустить контейнер, нужен образ (image). Образ — это неизменяемый слепок файловой системы: все файлы приложения, зависимости, конфигурация, нужная версия Java.
Соотношение простое:
- Образ — шаблон (как класс в Java). Он неизменен, хранится на диске или в реестре.
- Контейнер — запущенный экземпляр образа (как объект). Их может быть несколько из одного образа.
Один образ myapp:1.0 можно запустить как один контейнер на ноутбуке разработчика, второй — на тестовом сервере, третий — в production. Каждый получает идентичное окружение.
Где это помогает бэкендеру
Одинаковое окружение везде. Dev, staging, production работают на одном образе. «На моей машине работает» перестаёт быть аргументом.
Быстрый старт и остановка. Контейнер с JVM стартует за несколько секунд. Это удобно как для разработки, так и для горизонтального масштабирования под нагрузкой.
Изоляция зависимостей. Два приложения требуют разные версии одной библиотеки — каждое запускается в своём контейнере без конфликтов.
Воспроизводимая сборка. Образ собирается по инструкции (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>
docker run скачивает образ из реестра (если его нет локально), создаёт контейнер и запускает указанную команду. docker ps показывает, что сейчас работает.
Реестр по умолчанию — Docker Hub. В браузере на него смотрят через hub.docker.com, а качает Docker с docker.io — это и есть имя реестра, которое прописывают в настройках, когда подменяют его на внутреннее зеркало. Там хранятся официальные образы: eclipse-temurin, postgres, nginx и сотни других.
Теперь короткая формула читается целиком: контейнер = изолированный процесс + своя файловая система.
Но есть одна оговорка, на которой новичок спотыкается первым, и обещание «на моей машине работает» ломается именно о неё: у образа есть архитектура процессора. Ноутбук на 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 проходит там, где PostgreSQL упадёт на первом же jsonb, и наоборот; проверять нужно на настоящей базе, и Testcontainers поднимает её из теста тем же Docker, что описан выше.
@SpringBootTest
@Testcontainers
class OrderRepositoryTest {
@Container
@ServiceConnection
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:17");
@Test
void savesOrder() { ... }
}
Библиотека находит Docker на машине, скачивает образ, запускает контейнер на случайном свободном порту, ждёт, пока база ответит, и подставляет адрес в Spring через @ServiceConnection; после тестов контейнер удаляется. То же для Kafka, RabbitMQ, Redis, Elasticsearch, Keycloak и любого образа через GenericContainer. Один контейнер на класс тестов (static) стартует за секунды, на каждый тест дороже без нужды.
Три вещи, которые узнают из ошибок. Рядом со своими контейнерами Testcontainers запускает служебный контейнер Ryuk: он следит за тестовым процессом и удаляет всё, что тот оставил, если JVM убили; в 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 на узле, о чём статья про безопасность образов), либо используют управляемый сервис контейнеров для тестов. Это первый вопрос при заведении конвейера, а не последний.
Коротко
- Контейнер — изолированный процесс с собственной файловой системой; изоляция строится на
namespacesиcgroupsядра Linux. - Контейнер использует ядро хоста — он легче и быстрее виртуальной машины, но изолирует только на уровне процессов. Изоляция контейнера не равна изоляции виртуальной машины: ядро общее, процесс внутри по умолчанию
root, а без лимитов он видит всю память и все ядра хоста. - Образ — неизменяемый шаблон; контейнер — запущенный экземпляр образа (один образ, много контейнеров).
- Docker решает проблему «на моей машине работает»: dev, staging и production работают из одного образа.
- Образ описывается файлом
Dockerfileи воспроизводим на любой машине. Базовые операции:docker run,docker ps,docker stop,docker rm. - Тесты идут на настоящей базе через Testcontainers: контейнер на класс тестов с
@ServiceConnection, Ryuk чистит за упавшей JVM, повторное использование ускоряет локально, префикс реестра вtestcontainers.properties. - Всё хозяйство лежит в
/var/lib/docker(на macOS и Windows — внутри виртуальной машины): смотретьdocker system df, чиститьimage pruneиbuilder prune, аsystem prune -a --volumesуносит и данные баз. - Контейнеры — это стандарт OCI, а Docker его реализация: внутри containerd и runc, Kubernetes с версии 1.24 ходит в containerd напрямую, а образы собирают и без Docker.
- Обещание «работает везде» ломает архитектура процессора:
arm64на ноутбуке противamd64на сервере, отсюдаexec format errorи сборка под две платформы.
Что почитать дальше
- Образы и Dockerfile — как устроены слои образа и как написать первый
Dockerfile. - Контейнеры: запуск и управление — флаги
docker run, переменные окружения, порты, жизненный цикл контейнера. - Spring Boot в контейнере — упакуем реальное Spring Boot-приложение в образ шаг за шагом.