Установка Docker и Docker Compose в Debian и Ubuntu

Опубликовано 14 мин. чтения

Почему docker.io в Debian 12 слишком стар, а в Ubuntu вполне годится, как подключить официальный репозиторий с keyring вместо apt-key, почему Compose стал плагином и отчего группа docker фактически равна root.

Установить Docker можно за пять минут. Установить его так, чтобы через год сервер по-прежнему получал свежие обновления безопасности, системный диск не заполнялся до отказа, а права root случайно не оказались у каждого пользователя, займёт немного больше времени. Эта статья посвящена второму варианту и проверена на Debian 13 (Trixie), Debian 12 (Bookworm), Ubuntu 24.04 (Noble) и Ubuntu 22.04 (Jammy).

docker.io или Docker CE: разница, о которой мало кто говорит честно

Получить Docker можно двумя путями. Пакет docker.io приходит из репозиториев самого дистрибутива, его собирают и сопровождают Debian и Ubuntu. Пакет docker-ce приходит из репозитория компании Docker. Внутри обоих одно и то же программное обеспечение, но очень разного возраста.

Сейчас в репозиториях дистрибутивов лежат такие версии (замерено командой apt-cache policy в свежих контейнерах):

Системаdocker.io в репозитории дистрибутива
Debian 13 (Trixie)26.1.5
Debian 12 (Bookworm)20.10.24
Ubuntu 24.04 (Noble)29.1.3
Ubuntu 22.04 (Jammy)29.1.3

Вот это и есть главное, но почти во всех руководствах данный факт сводят к одной общей рекомендации. На деле картина сложнее:

  • Ubuntu 24.04 и 22.04: docker.io находится на версии 29.1.3, то есть практически на уровне текущего upstream. Если особых требований нет, пакет из дистрибутива здесь можно брать со спокойной совестью. Обновления безопасности будут приходить обычным каналом Ubuntu.
  • Debian 13: версия 26.1.5 вполне рабочая, но заметно отстаёт от upstream. Для большинства задач её хватает.
  • Debian 12: вот здесь и кроется проблема. Ветка 20.10.24 в upstream уже несколько лет как завершила свой жизненный цикл. Debian, конечно, вносит исправления безопасности, но многих современных возможностей там просто нет, в частности актуальной версии BuildKit и части совместимости с Compose.

К этому добавляется практическая разница: docker-ce приносит Buildx и Compose отдельными пакетами плагинов, подобранными под движок. В случае с docker.io эти части приходится собирать самостоятельно из docker-buildx и docker-compose-v2, причём docker-compose-v2 есть только в репозиториях Ubuntu, а в Debian такого пакета нет вовсе.

Чего сделать нельзя: держать оба варианта параллельно. Пакет containerd.io из репозитория Docker конфликтует с пакетом containerd из дистрибутива. Придётся выбрать что-то одно.

Практическое правило: в Ubuntu docker.io вполне законный выбор. В Debian 12 нет. Для серверов, на которых работают стеки Compose с современным синтаксисом, Docker CE везде оказывается лучшим решением.

Сначала убрать старые пакеты, только потом всё остальное

Если Docker в каком-либо виде на сервере уже есть, его нужно убрать, иначе установка споткнётся о конфликты пакетов. Следующий цикл удаляет всех обычных подозреваемых и заодно спокойно переживает пакеты, которые не установлены или в вашем дистрибутиве вообще отсутствуют:

for pkg in docker.io docker-doc docker-compose docker-compose-v2 podman-docker containerd runc; do sudo apt-get remove -y $pkg || true; done

|| true в теле цикла не косметика, а необходимость. В Debian на записи docker-compose-v2 команда apt-get обрывается сообщением E: Unable to locate package docker-compose-v2 и кодом возврата 100, ведь этот пакет существует исключительно в репозиториях Ubuntu, а не в bookworm или trixie (и не в bullseye, где вдобавок нет podman-docker). Цикл, конечно, пойдёт дальше, но оставит после себя код возврата, отличный от нуля, и именно на этом умирает скрипт с set -e или цепочка через &&. То есть сообщения вида «Unable to locate package» здесь нормальны и их можно игнорировать: ровно так же поступает и официальная документация Docker.

Важно понимать: при этом ничего не теряется. Ваши образы, контейнеры и тома лежат в /var/lib/docker, а этот каталог apt-get remove не трогает. После установки Docker CE контейнеры окажутся на месте. По-настоящему удаляет только sudo rm -rf /var/lib/docker, и это уже необратимо.

Как правильно положить ключ: apt-key остался в прошлом

Во многих руководствах в сети до сих пор встречается такая строка:

curl -fsSL https://download.docker.com/linux/debian/gpg | sudo apt-key add -

Ни на одной из четырёх рассматриваемых систем это больше не имеет смысла. apt-key объявлен устаревшим, а в Debian 13 его вообще нет. Причина не косметическая: ключ в /etc/apt/trusted.gpg подписывает все репозитории, а не только тот, для которого он предназначался. Скомпрометированное зеркало сможет таким образом подсунуть вам произвольные пакеты.

Правильный путь: отдельная связка ключей в /etc/apt/keyrings/, которая через Signed-By привязывается ровно к одному источнику.

sudo apt-get update
sudo apt-get install -y ca-certificates curl gnupg
sudo install -m 0755 -d /etc/apt/keyrings

Следующая команда забирает нужный ключ и работает как в Debian, так и в Ubuntu, потому что читает идентификатор дистрибутива из /etc/os-release:

sudo curl -fsSL "https://download.docker.com/linux/$(. /etc/os-release && echo "$ID")/gpg" -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc

А теперь шаг, который практически никто не описывает: проверьте отпечаток ключа, прежде чем доверять ему свою систему.

gpg --show-keys /etc/apt/keyrings/docker.asc

В выводе должны присутствовать отпечаток 9DC8 5822 9FC7 DD38 854A E2D8 8D81 803C 0EBF CD88 и идентификатор Docker Release (CE deb) <docker@docker.com>. Если совпадения нет, остановитесь. Значит, что-то не в порядке с вашим соединением или с источником.

Установка Docker CE одним фрагментом для всех четырёх систем

Официальная документация показывает отдельные блоки для Debian и для Ubuntu. В этом нет необходимости. Блок ниже записывает источник пакетов в современном формате deb822 и сам определяет дистрибутив, кодовое имя выпуска и архитектуру:

sudo tee /etc/apt/sources.list.d/docker.sources > /dev/null <<EOF
Types: deb
URIs: https://download.docker.com/linux/$(. /etc/os-release && echo "$ID")
Suites: $(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}")
Components: stable
Architectures: $(dpkg --print-architecture)
Signed-By: /etc/apt/keyrings/docker.asc
EOF

Две детали, которые экономят время. Первая: ${UBUNTU_CODENAME:-$VERSION_CODENAME}. В производных от Ubuntu системах вроде Linux Mint в VERSION_CODENAME записано имя производного дистрибутива, а не имя выпуска Ubuntu. Вторая: строка Architectures. Без неё на системах с включённой чужой архитектурой i386 apt выдаёт длинное предупреждение об отсутствующих списках пакетов.

Проверьте результат, прежде чем идти дальше:

cat /etc/apt/sources.list.d/docker.sources

В строке Suites должно стоять trixie, bookworm, noble или jammy. Если там что-то другое, следующий шаг завершится ошибкой. Дальше идут обновление списков и установка:

sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

Эти пять пакетов: демон, утилита командной строки, среда выполнения контейнеров, современный сборщик образов и Compose.

Как понять, что всё действительно работает

То, что apt-get отработал без ошибок, говорит лишь о том, что файлы легли на диск. Следующие четыре проверки показывают, работает ли система на самом деле.

Первое: доходит ли клиент до демона?

docker version

Решает не раздел Client, а то, что под ним появляется раздел Server: Docker Engine - Community с номером версии. Если его нет, значит демон не запущен либо у вас нет прав на доступ к сокету.

Второе: какой драйвер хранения активен?

docker info --format '{{.Driver}}'
docker info --format '{{.CgroupVersion}}'

Здесь скрывается новшество, на которое многие старые руководства отвечают неверно. Начиная с Docker Engine 29 при новых установках по умолчанию включено хранилище образов containerd. Драйвер тогда называется overlayfs, а не overlay2. Оба значения в порядке. Чего вам не хочется увидеть, так это vfs: этот аварийный драйвер копирует каждый слой целиком, съедает кратно больше дискового пространства и мучительно медленный. Появляется он обычно тогда, когда Docker работает в окружении без нужной поддержки со стороны ядра. Версия cgroup на всех четырёх системах должна быть равна 2.

Если вы обновили уже работавшую систему и вдруг кажется, что все образы пропали: они не удалены. При смене хранилища образов содержимое другого склада просто перестаёт показываться и снова появляется после переключения назад. Переключиться назад можно через /etc/docker/daemon.json:

{
  "features": {
    "containerd-snapshotter": false
  }
}

Третье: запускается ли контейнер на самом деле?

docker run --rm hello-world

Четвёртое: работают ли сеть и разрешение имён внутри контейнера? Этой проверки нет почти ни в одном руководстве, хотя именно здесь возникает большинство последующих проблем:

docker run --rm alpine:3 ping -c 2 1.1.1.1
docker run --rm alpine:3 nslookup deb.debian.org

Если ping отвечает, а разрешение имён не работает, чаще всего дело в DNS-сервере, который слушает только на 127.0.0.53. Из контейнера этот адрес недоступен. Помогает запись "dns": ["9.9.9.9"] в /etc/docker/daemon.json и перезапуск демона.

Compose теперь плагин, а не отдельная программа

Старый docker-compose через дефис был самостоятельной программой на Python. Его поддержка завершена, и он больше не поставляется. На смену пришёл плагин, написанный на Go, который вызывается как подкоманда Docker-CLI, то есть docker compose через пробел.

docker compose version

Замечание о номере версии, потому что здесь регулярно возникает путаница: выражение «Compose V2» обозначает переписанную на Go реализацию, а не номер версии. Сегодня вывод показывает версию из ветки 5.x. Это правильно, и речь идёт вовсе не о другом продукте.

При переходе бросаются в глаза две вещи. Во-первых, ключ version: в начале docker-compose.yml стал лишним и вызывает предупреждение:

WARN[0000] docker-compose.yml: the attribute `version` is obsolete, it will be ignored, please remove it to avoid potential confusion

Просто удалите эту строку. Во-вторых, меняются имена: Compose выводит имя проекта из имени каталога и создаёт контейнеры с дефисом вместо подчёркивания, то есть myproject-web-1 вместо myproject_web_1. Скрипты, которые обращаются к контейнерам по жёстко заданным именам, из-за этого ломаются. В таких случаях задавайте имя проекта явно через name: в файле Compose или через -p.

Если вы сознательно хотите остаться на пакетах дистрибутива, нужный пакет называется docker-compose-v2 и даёт ту же самую подкоманду. Правда, доступен он только в репозиториях Ubuntu. Проверьте доступную версию заранее:

apt-cache policy docker-compose-v2

В Ubuntu 24.04 и 22.04 появится таблица с установленной версией и кандидатом. В Debian 13 и Debian 12 команда не выводит ничего: пустой вывод с кодом возврата 0. Это не ошибка, а ответ. В Debian такого пакета нет, и путь к Compose там идёт через docker-compose-plugin из репозитория Docker.

Группа docker — это root, просто окольным путём

Чтобы обычный пользователь мог работать с Docker без sudo, его обычно добавляют в группу docker:

sudo groupadd -f docker
sudo usermod -aG docker $USER

Членство в группе вступает в силу только при новом входе в систему. Проверить это после повторного входа можно командой id -nG. Кто не хочет перезаходить, запускает командой newgrp docker оболочку с новой группой.

А теперь то, что нужно понять обязательно: членство в группе docker равносильно правам root на всём сервере. Это не теоретическая оценка, а прямое следствие того, как всё устроено. Тот, кому разрешено общаться с сокетом Docker, может отдавать демону (а он работает от root) любые задания. Хватает одной команды:

docker run -it -v /:/hostfs alpine:3 chroot /hostfs sh

Результат: оболочка root на хост-системе, без sudo, без запроса пароля, без записи в журнале sudo. Прочитать /etc/shadow, положить свой SSH-ключ, подменить службы: возможно всё. Документация Docker формулирует это коротко и недвусмысленно: «The docker group grants root-level privileges to the user.»

Практические выводы для сервера, доступного из интернета:

  • Добавляйте в группу только те учётные записи, которым вы и так доверили бы root.
  • Пользователь, от имени которого работает веб-приложение или раннер CI, к таким не относится. Иначе взлом приложения автоматически становится взломом сервера.
  • Если вам нужна прослеживаемость, откажитесь от группы и вызывайте Docker через sudo docker. Тогда сам вызов хотя бы попадёт в журнал.
  • Для настоящего разделения есть режим rootless. Он настраивается через пакет docker-ce-rootless-extras и утилиту dockerd-rootless-setuptool.sh install, дополнительно требуется uidmap. Цена вопроса: порты ниже 1024 без дополнительной настройки занять не получится, а часть сетевых функций ведёт себя иначе.

Автозапуск: ловушка называется docker.socket

Стандартная команда известна:

sudo systemctl enable --now docker.service
sudo systemctl enable --now containerd.service

Менее известно, почему отключение часто не срабатывает. Помимо docker.service, Docker приносит с собой ещё и docker.socket. Этот юнит слушает сокет и при первом же обращении автоматически поднимает демон. Так что если вы выполнили systemctl disable docker.service, а затем обнаружили, что Docker всё равно работает, никакой мистики здесь нет: первый же docker ps поднял демон через сокет-юнит. Для полного отключения нужны оба:

sudo systemctl disable --now docker.service docker.socket

Проверить состояние можно командой systemctl is-enabled docker.service, она должна вывести enabled.

Второй момент касается ваших контейнеров. Вернётся ли контейнер после перезагрузки, решает не systemd, а политика перезапуска. И здесь есть разница, которая регулярно застаёт врасплох: always поднимает контейнер заново даже тогда, когда вы намеренно остановили его перед перезагрузкой. unless-stopped уважает вашу ручную остановку и сохраняет её через перезагрузку. Для серверов unless-stopped почти всегда правильный выбор:

services:
  web:
    image: nginx:stable
    restart: unless-stopped

Проверкой здесь служит не docker ps, а настоящая перезагрузка сервера с последующим контролем.

Ротация журналов: самая частая причина переполнения системного диска

По умолчанию Docker пишет вывод каждого контейнера в файл JSON внутри /var/lib/docker/containers/. Этот файл по умолчанию растёт без ограничений. Разговорчивый reverse proxy способен за несколько месяцев накопить десятки гигабайт, пока сервер не встанет с сообщением no space left on device. Найти виновника потом непросто, потому что du в каталоге приложения ничего подозрительного не показывает.

Решение стоит внедрять на каждом сервере, причём до появления первой проблемы:

sudo mkdir -p /etc/docker
sudo tee /etc/docker/daemon.json > /dev/null <<'EOF'
{
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "10m",
    "max-file": "3"
  }
}
EOF

Если daemon.json уже существует, эта команда его перезапишет. Загляните в файл заранее и при необходимости добавьте ключи вручную. Затем:

sudo systemctl restart docker

Три места, где всё равно может пойти не так:

  • Значения должны стоять в кавычках как строки. Запись "max-file": 3 без кавычек не даст демону запуститься.
  • Настройка действует только для вновь создаваемых контейнеров. Существующие сохраняют старую конфигурацию, пока их не пересоздадут, в случае Compose через docker compose up -d --force-recreate.
  • Удалять переполненный файл журнала командой rm бесполезно, место на диске не вернётся, потому что демон всё ещё держит файл открытым. Используйте вместо этого sudo truncate -s 0 <ПУТЬ>.

Действует ли настройка для конкретного контейнера, проверяется так:

docker inspect --format '{{json .HostConfig.LogConfig}}' mycontainer

Уборка через docker system prune без потери данных

Неиспользуемые образы, оборванные сборки и cache BuildKit складываются в заметный объём. Сначала стоит посмотреть, куда уходит место:

docker system df
docker system df -v

Стандартная команда уборки удаляет остановленные контейнеры, неиспользуемые сети, образы без имён и build-cache:

docker system prune

Два ключа заслуживают уважения. -a дополнительно удаляет все образы, которые в данный момент не используются ни одним контейнером, то есть и бережно поддерживаемые базовые образы. На сервере с узким каналом их повторная загрузка может занять немало времени. Заметно опаснее --volumes: этот ключ убирает именованные тома, у которых нет связанного контейнера. Если ваш контейнер с базой данных только что удалён, а том всё ещё содержит данные, после этого данных не станет. Корзины здесь нет.

Никогда не используйте --volumes в автоматическом задании по уборке.

Разумно задать отсрочку по времени, чтобы исчезало только действительно старое:

docker system prune -a --filter "until=168h"
docker builder prune --filter "until=168h"

В качестве еженедельного задания, бережного к ресурсам ночью и без удаления томов, хватает одной строки в /etc/cron.d/docker-prune:

15 4 * * 0 root /usr/bin/docker system prune -af --filter "until=168h" > /dev/null 2>&1

Подтверждением успеха служит повторный docker system df с уменьшившимся столбцом «RECLAIMABLE».

Сообщения об ошибках дословно и что за ними стоит

permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock
Пользователь не входит в группу docker, либо членство в группе ещё не действует в текущем сеансе. Перезайдите или выполните newgrp docker.

Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?
Демон не запущен. Причину и точную формулировку выдадут systemctl status docker и, подробнее, journalctl -u docker -n 50 --no-pager. Очень часто за этим стоит ошибочный /etc/docker/daemon.json. Этот файл должен быть корректным JSON, достаточно одной лишней запятой.

docker: 'compose' is not a docker command.
Плагина нет. Доустановите либо docker-compose-plugin из репозитория Docker, либо, только в Ubuntu, docker-compose-v2 из дистрибутива.

E: Conflicting values set for option Signed-By regarding source https://download.docker.com/linux/debian/ trixie: /etc/apt/keyrings/docker.asc != /etc/apt/keyrings/docker.gpg
Классика после проработки нескольких руководств подряд: одновременно существуют старый /etc/apt/sources.list.d/docker.list и новый docker.sources. Удалите старый файл и повторите sudo apt-get update. Общую картину даст ls -l /etc/apt/sources.list.d/.

The following signatures couldn't be verified because the public key is not available: NO_PUBKEY 8D81803C0EBFCD88
Ключ лежит не там, где его ожидает Signed-By, либо скачанный файл неполон (например, потому что proxy отдал HTML-страницу с ошибкой). Команда gpg --show-keys /etc/apt/keyrings/docker.asc сразу покажет, есть ли внутри вообще какой-нибудь ключ.

E: The repository 'https://download.docker.com/linux/debian trixie Release' does not have a Release file.
Кодовое имя не соответствует источнику. Такое случается на производных дистрибутивах и при смешивании руководств для Debian и Ubuntu. Проверьте строки URIs и Suites в docker.sources.

Bind for 0.0.0.0:80 failed: port is already allocated
Порт занят другой службой, часто напрямую установленным веб-сервером. Виновника назовёт sudo ss -tulpn | grep :80.

Docker и файрвол: пара слов о безопасности

Особенность, которая на публично доступном сервере может обойтись дорого: Docker вносит свои правила перенаправления в таблицу NAT и тем самым обходит правила, которые вы настроили в ufw. Контейнер, запущенный с -p 5432:5432, доступен из интернета, даже если ufw status нигде этот порт не разрешает. Так остаётся и сейчас, хотя Docker Engine 28 в целом ужесточил сетевое поведение и закрыл снаружи доступ к неопубликованным портам.

Самая простая и надёжная мера противодействия: службы, нужные только локально, явно привязывать к адресу loopback:

services:
  db:
    image: postgres:17
    ports:
      - "127.0.0.1:5432:5432"
    restart: unless-stopped

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

Коротко о главном

В Debian 12 в любом случае берите Docker CE, в Ubuntu можно выбирать между docker.io и Docker CE. Подключайте репозиторий отдельной связкой ключей с проверенным отпечатком, а не через apt-key. Используйте docker compose через пробел. Относитесь к группе docker как к выдаче прав root, потому что она ровно этим и является. И настройте ротацию журналов вместе с еженедельным заданием prune до того, как сервер впервые встанет из-за переполненного диска.

По теме: Настройка файрвола UFW в Debian и Ubuntu и Установка Nginx в Debian и Ubuntu.

Частые вопросы

Что ставить: docker.io или docker-ce?
В Debian 12 однозначно Docker CE, потому что пакет дистрибутива остался там на версии 20.10.24, а эта ветка в upstream давно завершила свой жизненный цикл. В Debian 13 docker.io даёт 26.1.5, в Ubuntu 24.04 и 22.04 даже 29.1.3, то есть практически актуальное состояние. Там пакет дистрибутива вполне законный выбор, если вы отдельно доустановите Buildx и Compose. Правда, пакет docker-compose-v2 есть только в репозиториях Ubuntu, а в Debian путь к Compose идёт через docker-compose-plugin из репозитория Docker.
Почему docker-compose через дефис больше не работает?
Старый docker-compose был самостоятельной программой на Python и больше не поставляется. Его преемник: плагин Docker-CLI, написанный на Go, который вызывается как docker compose через пробел. Он лежит в пакете docker-compose-plugin (репозиторий Docker) или, только в Ubuntu, в docker-compose-v2 из дистрибутива. Проверить можно так: docker compose version.
Действительно ли группа docker так опасна, как о ней говорят?
Да. Тот, кому разрешён доступ к сокету Docker, может отдавать работающему от root демону любые задания, например смонтировать корневой каталог хоста в контейнер и открыть в нём оболочку root. Документация Docker называет это root-level privileges. Добавляйте в группу только те учётные записи, которым вы и так доверяете права root, либо используйте режим rootless.
Почему docker info показывает overlayfs вместо overlay2?
Начиная с Docker Engine 29 при новых установках по умолчанию включено хранилище образов containerd. Его snapshotter называется overlayfs. Это правильно и ошибкой не является. Проблемным было бы только значение vfs: оно указывает на отсутствие нужной поддержки со стороны ядра и расходует очень много дискового пространства.
После обновления пропали все образы, они удалены?
Как правило, нет. При переключении между классическим хранилищем образов и хранилищем containerd содержимое другого склада просто перестаёт отображаться, а сами данные остаются на диске. Переключитесь для пробы обратно через features containerd-snapshotter в /etc/docker/daemon.json, и образы снова появятся.
Как не дать журналам контейнеров заполнить диск?
Драйвер json-file по умолчанию файлы не ротирует. Впишите в /etc/docker/daemon.json блок log-opts со значениями max-size 10m и max-file 3, значения должны стоять в кавычках как строки. После systemctl restart docker настройка действует только для вновь создаваемых контейнеров, существующие нужно пересоздать командой docker compose up -d --force-recreate.
Безопасна ли команда docker system prune?
Без ключей в основном да: она удаляет остановленные контейнеры, неиспользуемые сети, безымянные образы и build-cache. Ключ -a дополнительно удаляет все образы, которые в данный момент не используются. Опасен ключ --volumes, потому что с ним исчезают именованные тома без связанного контейнера, то есть, возможно, ваша база данных. В автоматических заданиях ключу --volumes не место.

Docker Docker Compose Debian Ubuntu Linux-сервер containerd apt Администрирование серверов