Установка Docker Compose на Ubuntu 24.04
Установите Docker Engine и Compose v2 на Ubuntu 24.04. Узнайте, как настроить compose.yml, избежать проблем с ufw и правильно делать backup volumes.
Что вы создаете
Docker Compose является базовой средой для большинства сервисов на этом сайте. Nextcloud, Vaultwarden, n8n, Immich, Rocket.Chat — инструкции к каждому из этих приложений начинаются с команды «создайте этот compose file», и данная страница объясняет назначение этого файла. Вы установите Docker Engine и плагин Compose v2 из официального apt-репозитория Docker на Ubuntu 24.04. Затем вы развернете стек из двух сервисов — Miniflux (легкий RSS-ридер) и PostgreSQL. Эта пара демонстрирует все паттерны, используемые в более крупных приложениях: фиксация версий образов (pinned images), база данных с проверкой состояния (healthcheck), именованный том (named volume), секреты в файле .env и проброс порта только на localhost.
Установка занимает пять минут. В остальной части руководства рассматриваются критические аспекты: права группы docker, эквивалентные root, обход правил ufw опубликованными портами и флаг в docker compose down, который удаляет базу данных без подтверждения.
Предварительные требования: чистая Ubuntu 24.04 KVM VPS, пользователь с правами sudo и не менее 1 ГБ оперативной памяти. Наличие установленного Docker также допустимо — в первом разделе описано, что необходимо удалить.
Устанавливайте из репозитория Docker, а не из репозитория Ubuntu
Перед выполнением первой команды необходимо исключить две ошибки. Пакет docker.io от Ubuntu работает, но его версии отстают от релизов Docker. Также в нем отсутствует структура плагинов, которую ожидают другие компоненты. Отдельный бинарный файл docker-compose (через дефис) — это Compose v1 на базе Python. Его поддержка прекращена в 2023 году, поэтому старые руководства часто не работают. Современный Compose — это docker compose (через пробел), он является CLI-плагином и устанавливается из того же репозитория, что и engine.
Если эти компоненты уже установлены в системе, сначала удалите их. Удалите, включая docker-compose-v2 (пакет плагина от Ubuntu), чтобы все компоненты поступали из одного репозитория:
sudo apt remove -y docker.io docker-compose docker-compose-v2 docker-doc podman-docker containerd runcНа чистом VPS результатом выполнения команды Package 'docker.io' is not installed, so not removed будет стандартный вывод. Затем добавьте репозиторий Docker и выполните установку:
sudo apt update
sudo apt install -y ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-pluginПроверьте все три уровня:
docker --version
docker compose version
sudo docker run --rm hello-worldПервые два пункта выведут строки с версиями. Docker Compose version v2.x.x подтверждает наличие плагина вместо устаревшего бинарного файла v1. Команда hello-world должна завершиться выводом Hello from Docker!. Пакет включает службу при загрузке; systemctl is-enabled docker выведет enabled.
Группа docker принадлежит root — учитывайте это при принятии решений
В данный момент любая команда docker требует прав sudo, так как сокет демона по пути /var/run/docker.sock принадлежит пользователю root и группе docker. Без членства в этой группе вы получите самую часто встречающуюся ошибку Docker:
permission denied while trying to connect to the Docker daemon socket at
unix:///var/run/docker.sockСтандартное решение:
sudo usermod -aG docker $USERПрава группы применяются при входе в систему, поэтому ошибка сохранится в текущем сеансе shell. Выполните newgrp docker для текущего сеанса или перезайдите в систему; после этого команда id должна показать docker в списке ваших групп.
Теперь прямое утверждение: членство в группе docker дает права root на хост-системе. Это не «почти root» и не «повышенные права» — это root. Любой пользователь в этой группе может выполнить docker run --rm -it -v /:/host alpine chroot /host и получить полный контроль над файловой системой без ввода пароля. Группа создана для удобства, а не для обеспечения изоляции.
Настоящей альтернативой является rootless mode в Docker — в этом режиме демон запускается от имени вашего непривилегированного пользователя. Это влечет за собой следующие издержки: для использования портов ниже 1024 требуется дополнительная настройка, сетевое взаимодействие идет через прослойку в user space с заметными накладными расходами, а некоторые образы работают некорректно без реальных прав root. На VPS с одним администратором, у которого уже есть sudo, добавление в группу на практике ничего не меняет. Все руководства в этом разделе исходят из этого предположения, но никогда не передавайте права группы так, будто это менее опасно, чем sudo.
Анатомия compose файла
Для каждого стека используйте отдельную директорию. Имя директории становится именем проекта, которое добавляется в качестве префикса к контейнерам, сетям и томам:
sudo mkdir -p /opt/miniflux && sudo chown $USER /opt/miniflux && cd /opt/minifluxСоздавайте compose.yml (современное название; docker-compose.yml все еще работает). Не используйте устаревший ключ version: — он не актуален, и Compose выдаст предупреждение при его обнаружении.
services:
miniflux:
image: miniflux/miniflux:2.2.9
restart: unless-stopped
ports:
- "127.0.0.1:8080:8080"
environment:
- DATABASE_URL=postgres://miniflux:${POSTGRES_PASSWORD}@db/miniflux?sslmode=disable
- RUN_MIGRATIONS=1
- CREATE_ADMIN=1
- ADMIN_USERNAME=admin
- ADMIN_PASSWORD=${ADMIN_PASSWORD}
depends_on:
db:
condition: service_healthy
db:
image: postgres:16-alpine
restart: unless-stopped
environment:
- POSTGRES_USER=miniflux
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
- POSTGRES_DB=miniflux
volumes:
- db-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD", "pg_isready", "-U", "miniflux", "-d", "miniflux"]
interval: 10s
timeout: 5s
retries: 5
volumes:
db-data:Каждая строка выше — это осознанное решение. Применяйте их по одной.
Фиксация версий образов — использование :latest и pull приводит к неконтролируемым обновлениям
Используйте postgres:16-alpine вместо postgres:latest. Тег не является статичным: при каждом выполнении pull :latest разрешается в ту версию, которую разработчик опубликовал последней. Если сочетать это с привычкой к регулярным обновлениям (docker compose pull && docker compose up -d), то при использовании :latest переход на мажорную версию произойдет в момент ее выхода в upstream, а не тогда, когда вы этого захотите. Для PostgreSQL это не теория: внезапный переход с 16 на 17 приведет к циклической ошибке запуска контейнера из-за несовместимости директории с данными. Обновление мажорных версий Postgres требует выполнения dump и restore, а не просто перезапуска.
Фиксируйте как минимум мажорную версию (postgres:16-alpine соответствует патч-релизам 16.x), а приложения фиксируйте на конкретном релизе, например miniflux/miniflux:2.2.9. Проверяйте страницу релизов проекта и используйте актуальную версию при написании файла. В таком случае обновление превращается в намеренное изменение одной строки, которое можно отследить в git diff.
Публикация на 127.0.0.1, так как Docker обходит ufw
Используйте формат "127.0.0.1:8080:8080" — адрес хоста, порт хоста, порт контейнера. В большинстве руководств пишут "8080:8080", что является сокращением для 0.0.0.0:8080:8080 (прослушивание всех интерфейсов, включая публичный).
В этом заключается ловушка, на которой попадаются почти все. Docker публикует порт, создавая правило DNAT. Это правило перенаправляет пакет на внутренний IP контейнера до фильтрации. Таким образом, пакет идет по пути FORWARD и не проходит через INPUT, где работают ваши правила ufw. sudo ufw deny 8080 сообщает об успехе, ufw status показывает, что порт закрыт, но сервис по-прежнему доступен из всего интернета. Ваш файрвол не сломан; он обходится по определению. Почему Docker обходит ufw и как правильно фильтровать трафик контейнеров подробно описывает этот механизм и решение DOCKER-USER для портов, которые должны оставаться публичными.
Способ избежать этой проблемы: привязывайте опубликованные порты к 127.0.0.1 (если на то нет веских причин) и используйте реверс-прокси для всего, что должно быть доступно извне. Именно это описано в Руководство по реверс-прокси Traefik как следующий шаг: один контейнер занимает порты 80 и 443 и маршрутизирует трафик по именам хостов с использованием TLS. (Если вы переходите с Traefik v2? Руководство по миграции с Traefik v2 на v3 содержит информацию об изменениях в именах и правилах.)
Проверьте привязку после запуска стека: sudo ss -tlnp | grep 8080 должен показывать 127.0.0.1:8080, а не 0.0.0.0:8080 или *:8080.
Именованные тома против bind mounts
db-data:/var/lib/postgresql/data — это именованный том: Docker создает и управляет директорией в /var/lib/docker/volumes/ и монтирует ее в контейнер. Альтернативой является bind mount, ./data:/var/lib/postgresql/data, который сопоставляет выбранный вами путь на хосте.
Практическое разделение: именованные тома только для данных, с которыми работает контейнер — прежде всего базы данных, так как Docker инициализирует том с правами владения, которые ожидает образ, и права доступа к файлам работают корректно. Bind mounts для файлов, которые вы редактируете на хосте — конфигурационные файлы, медиабиблиотеки (например, через rsync) или любые файлы, путь к которым должен быть явным. Классическая ошибка bind mount — права владения: контейнер запущен от UID 999, а директория на хосте принадлежит UID 1000, из-за чего приложение падает при запуске с ошибкой permission denied в логах. Использование именованных томов устраняет этот тип ошибок, но данные хранятся по пути, управляемому Docker (подробнее ниже).
environment и .env — не храните секреты в git
Переменные из ${POSTGRES_PASSWORD} не читаются из вашей оболочки; Compose берет их из файла .env, расположенного рядом с compose.yml. Создайте его:
cat > .env <<'EOF'
POSTGRES_PASSWORD=change-me-to-something-long
ADMIN_PASSWORD=change-me-too
EOF
chmod 600 .env
echo ".env" >> .gitignoreГенерируйте реальные значения с помощью openssl rand -hex 24. Используйте hex вместо base64: этот пароль попадает в строку подключения DATABASE_URL, а символы base64 (/, +, =) нарушают парсинг URL. Это приводит к ошибке аутентификации, а не к синтаксической ошибке, что тратит время на отладку. Строку .gitignore нужно добавить до первого коммита: compose файл можно безопасно публиковать и версионировать, но файл .env — нет. Секрет, попавший в историю git, считается скомпрометированным и требует ротации. Если запустить стек с отсутствующей переменной, Compose выдаст предупреждение и продолжит работу с пустой строкой, что для пароля Postgres означает сбой развертывания:
WARN[0000] The "POSTGRES_PASSWORD" variable is not set. Defaulting to a blank string.Команда docker compose config выводит полностью интерполированный файл — это самый быстрый способ проверить, какие данные фактически получат контейнеры; помните, что вывод содержит ваши секреты.
depends_on не ждет завершения запуска — если не добавить healthcheck
Обычный depends_on: [db] управляет только порядком запуска: Compose сначала запускает Postgres, а затем приложение, но Postgres может быть еще не готов принимать соединения. Приложение обращается к базе, получает отказ и завершает работу или пытается перезапуститься, в зависимости от реализации.
Надежный способ описан в примере выше: сервис db определяет healthcheck (в Postgres есть встроенный механизм pg_isready для этих целей), а приложение объявляет зависимость depends_on с условием condition: service_healthy. Compose запускает базу данных, проверяет состояние каждые 10 секунд и запускает Miniflux только после успешного прохождения проверки. Если база данных не переходит в состояние healthy (из-за неверного пароля или поврежденного тома), приложение не запустится, а Compose укажет, какая зависимость не прошла проверку:
dependency failed to start: container miniflux-db-1 is unhealthyЭто сообщение указывает на docker compose logs db, где и содержится реальная ошибка.
restart: unless-stopped
Параметр restart: unless-stopped для обоих сервисов означает, что контейнеры перезапустятся после сбоя или после перезагрузки VPS, но останутся остановленными, если вы намеренно выполнили docker compose stop. Альтернативный вариант always перезапускает контейнеры даже после ручной остановки, что требуется редко. Без политики перезапуска перезагрузка системы после обновления ядра в 4 утра незаметно остановит ваши сервисы до тех пор, пока вы не обнаружите проблему.
Ежедневные операции
Все ежедневные задачи выполняются пятью командами из директории проекта.
docker compose up -d # create and start; idempotent, recreates only what changed
docker compose ps # status, ports, and health of this project's containers
docker compose logs -f miniflux # follow one service's logs; --tail 100 for recent history
docker compose pull && docker compose up -d # upgrade to the pinned tags
docker compose down # stop and remove containers and the networkup -d можно запускать многократно — команда сравнивает состояние файлов с текущим состоянием системы и вносит изменения только в те сервисы, чьи конфигурации или образы изменились. Пара команд для обновления загружает то, на что указывают ваши закрепленные (pinned) теги: патчи до версии postgres:16-alpine; при использовании точных тегов обновления не произойдет, пока вы не измените тег вручную — в этом и заключается смысл. После обновлений накапливаются старые образы; используйте docker image prune -f для освобождения дискового пространства.
Теперь о деструктивных командах: docker compose down безопасна — контейнеры и сеть являются временными, а ваши данные хранятся в volume. docker compose down -v также удаляет именованные volumes. Это приведет к мгновенной потере базы данных без запроса подтверждения и возможности отмены. Флаг -v предназначен для удаления экспериментальных сред; в стеке с реальными данными используйте его так же осторожно, как rm -rf. У команды /var/lib/docker/volumes/ нет корзины.
Для разового запуска shell внутри запущенного контейнера: docker compose exec db psql -U miniflux предоставляет доступ к базе данных, а docker compose exec miniflux sh предоставляет доступ к shell в приложении.
Где фактически хранятся ваши данные
Именованные тома получают префикс проекта. Таким образом, db-data в директории miniflux превращается в miniflux_db-data:
docker volume ls
docker volume inspect miniflux_db-dataВывод команды inspect содержит важную строку:
"Mountpoint": "/var/lib/docker/volumes/miniflux_db-data/_data"Эта директория является базой данных. Она принадлежит пользователю root, находится в файловой системе хоста и сохраняется после down, обновлений и пересборки контейнеров. Именно эти данные должны быть включены в ваши резервные копии.
Резервное копирование именованного volume
Стандартный метод — использование временного контейнера, который монтирует volume в режиме read-only рядом с директорией хоста, после чего выполняется архивация через tar:
docker run --rm \
-v miniflux_db-data:/data:ro \
-v "$PWD":/backup \
alpine:3.22 tar czf /backup/miniflux-db-$(date +%F).tar.gz -C /data .Никаких установок и фоновых процессов; восстановление выполняется зеркально — tar xzf в новый пустой volume с обратным порядком монтирования.
Важное замечание для баз данных: архивация запущенной директории данных Postgres может зафиксировать состояние в процессе записи, что приведет к ошибке при запуске. Либо используйте docker compose stop на время работы tar, либо — что предпочтительнее — сделайте логический дамп, который гарантированно консистентен:
docker compose exec -T db pg_dump -U miniflux miniflux | gzip > miniflux-$(date +%F).sql.gzФлаг -T отключает псевдотерминал, который Compose выделяет по умолчанию — передача вывода дампа через TTY может повредить данные. Добавьте одну из этих команд в cron и копируйте результат с VPS; резервная копия на том же диске, где хранятся данные, является лишь дубликатом, а не бэкапом. Руководство по Nextcloud содержит полный график выполнения задач, основанный именно на этих двух методах.
Режимы сбоев и соответствующие сообщения
permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock — вы еще не состоите в группе docker или сессия была открыта до добавления в группу. id показывает ваши текущие группы; newgrp docker исправляет текущую оболочку, но для применения изменений необходимо выйти из системы и войти снова.
Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running? — другая проблема: демон остановлен. sudo systemctl status docker и sudo journalctl -u docker -n 50 указывают причину. На VPS типичной причиной является переполненный диск — проверьте df -h /var/lib/docker в первую очередь.
Bind for 127.0.0.1:8080 failed: port is already allocated — другой контейнер уже использует этот порт хоста. docker ps показывает, какой именно; обычно виновником является старый контейнер от экспериментального docker run, запущенного несколько недель назад. Если docker ps не показывает конфликтов, порт занят процессом, не относящимся к Docker: sudo ss -tlnp | grep 8080 называет этот процесс.
yaml: line 14: did not find expected key — ошибка отступов в указанной строке или непосредственно перед ней. Compose файлы используют формат YAML: отступ должен составлять два пробела, использование табуляции недопустимо. docker compose config проверяет файл без запуска сервисов; рекомендуется запускать эту команду после каждого изменения.
Проблема с ufw не выводит никаких ошибок, что делает её опасной: развертывание проходит успешно, ufw status выглядит корректно, но сканирование портов извне всё равно обнаруживает вашу базу данных. Перечитайте раздел о портах выше, проверьте каждую запись ports: на наличие префикса 127.0.0.1: и подтвердите результат с другой машины с помощью curl http://your-vps-ip:8080 — вы должны получить ответ connection refused.
Далее руководство по Traefik объясняет, как превратить этот стек в множество приложений за одним HTTPS-входом; что стоит хостить самостоятельно в 2026 году содержит список сервисов для запуска через него.
Игровой сервер, например сервер Minecraft на VPS, подходит в качестве первого проекта на Compose для практики.
FAQ
Why do I get "permission denied while trying to connect to the Docker daemon socket"?
Your user is not in the docker group, or was added after the current session began — membership only applies at login. Run sudo usermod -aG docker $USER, then newgrp docker or log out and back in, and confirm with id. The group grants root-equivalent access to the host, so only add users you would give sudo.
Does docker compose down delete my data?
Plain docker compose down does not — it removes containers and the project network; named volumes survive and the next up -d reattaches them. docker compose down -v is the destructive form: it deletes the named volumes, meaning your database, with no confirmation and no undo. Never run -v on a stack with real data unless you hold a verified backup.
What is the difference between docker-compose and docker compose?
docker-compose (hyphen) is Compose v1, a standalone Python binary that reached end of life in 2023 and should not be installed on new servers. docker compose (space) is Compose v2, a Go plugin for the Docker CLI, installed as docker-compose-plugin from Docker's apt repository. Commands and YAML are almost fully compatible, so when an old tutorial says docker-compose up, type docker compose up.
Why can I reach my Docker container from the internet even though ufw blocks the port?
Because Docker publishes ports with DNAT rules in iptables' PREROUTING chain, and the rewritten packets travel the FORWARD path through Docker's own chains — they never hit the INPUT chain where ufw's rules apply. ufw deny 8080 therefore does nothing to a published container port. Fix it at the source: publish to 127.0.0.1: and expose services through a reverse proxy instead.
Should I use a named volume or a bind mount?
Named volumes for data only the container touches — databases especially, since Docker sets the ownership the image expects and permissions just work. Bind mounts for files you also handle from the host: configs you edit, media you upload, anything whose path you want obvious. If a container fails at startup with permission denied on a bind mount, host-vs-container UID mismatch is the first thing to check.