Як встановити Docker Compose на Ubuntu 24.04
Покрокова інструкція: встановлення Docker Engine та Compose v2 на Ubuntu 24.04, налаштування compose.yml та уникнення конфліктів з ufw при публікації портів.
Що ви створюєте
Docker Compose є основою майже для всього іншого на цьому сайті. Nextcloud, Vaultwarden, n8n, Immich, Rocket.Chat — кожен із цих посібників починається з фрази "напишіть цей compose файл", і саме ця сторінка пояснює значення цього файлу. Ви встановите Docker Engine та плагін Compose v2 з офіційного apt-репозиторію Docker на Ubuntu 24.04, а потім розгорнете повноцінний стек із двох сервісів — Miniflux (невеликий RSS-рідер) та PostgreSQL. Ця пара демонструє всі патерни, які використовують більші додатки: фіксовані образи, база даних із перевіркою стану (healthcheck), іменований volume, секрети у файлі .env та порт, відкритий лише для localhost.
Встановлення триває п'ять хвилин. Решта цього посібника присвячена речам, які створюють проблеми пізніше: група docker, що фактично є root, відкриті порти, які обходять правила ufw, та один прапор у docker compose down, який видаляє вашу базу даних без підтвердження.
Передумови: чиста Ubuntu 24.04 KVM VPS, користувач із правами sudo та 1 GB RAM або більше. Встановлений 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 runcPackage 'docker.io' is not installed, so not removed — це стандартний вивід на новому VPS. Потім додайте репозиторій 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 потрібне додаткове налаштування, мережа працює через прошарок у просторі користувача (userspace shim) з помітними витратами ресурсів, а деякі образи можуть працювати некоректно без справжніх прав 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. Тег не є статичним: :latest щоразу при виконанні pull повертає ту версію, яку розробник опублікував останнім часом. Поєднайте це зі звичкою регулярного оновлення, яку ви зараз вивчите — docker compose pull && docker compose up -d — і :latest означає, що перехід на нову мажорну версію відбудеться тоді, коли її випустить розробник, а не коли ви цього захочете. Для PostgreSQL це не теорія: раптовий перехід з 16 на 17 призведе до циклічного перезапуску контейнера через несумісність директорії з даними, оскільки для оновлення мажорних версій Postgres потрібен дамп і відновлення, а не просто перезапуск.
Фіксуйте принаймні мажорну версію (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 показує, що порт заблоковано, але сервіс все одно доступний з усього інтернету. Ваш фаєрвол не зламаний; він обходиться за дизайном. Why Docker bypasses ufw, and how to filter container traffic for real описує цей механізм та рішення DOCKER-USER для портів, які мають залишатися публічними.
Звичка, яка усуває цю проблему: прив'язуйте (bind) опубліковані порти до 127.0.0.1, якщо у вас немає іншої причини, і використовуйте реверс-проксі для всього, що має бути доступне у світі. Саме це пропонує Traefik reverse proxy guide як наступний крок після цієї сторінки — один контейнер, який займає порти 80 та 443 і маршрутизує трафік до всього іншого за hostname з використанням TLS. (Ви переходите з Traefik v2? Traefik v2 to v3 migration guide описує зміни назв та правил.)
Перевірте прив'язку після запуску стека: sudo ss -tlnp | grep 8080 має показувати 127.0.0.1:8080, а не 0.0.0.0:8080 або *:8080.
Named volumes проти bind mounts
db-data:/var/lib/postgresql/data — це named volume: Docker створює та керує директорією в /var/lib/docker/volumes/ і монтує її в контейнер. Альтернативою є bind mount, ./data:/var/lib/postgresql/data, який відображає вибраний вами шлях на хості.
Практичне правило розподілу: named volumes тільки для даних, до яких торкається контейнер — насамперед бази даних, оскільки Docker ініціалізує том з правами власності, які очікує образ, і права доступу до файлів працюють коректно. Bind mounts для файлів, які ви редагуєте з хоста — конфігураційні файли, медіабібліотеки через rsync, або будь-що, шлях до чого ви хочете бачити явно. Класична помилка bind mount — права власності: контейнер працює під UID 999, ваша директорія на хості належить UID 1000, і додаток падає при запуску з помилкою permission denied у логах. Named volumes майже повністю усувають цей тип помилок, але ціною того, що дані зберігаються за шляхом, яким керує Docker (описано нижче).
environment та .env — не зберігайте секрети в git
${POSTGRES_PASSWORD} не зчитується з вашої оболонки (shell); 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 можна запускати повторно — команда порівнює стан файлів із поточним станом системи та оновлює лише ті сервіси, чий конфігураційний файл або образ змінилися. Пара команд для оновлення завантажує те, на що вказують ваші зафіксовані теги: патчі версій до postgres:16-alpine; для точних тегів оновлення не відбудеться, доки ви не зміните тег вручну — у цьому і полягає сенс. Після оновлень накопичуються старі образи; звільніть місце на диску за допомогою docker image prune -f.
Тепер про деструктивні операції: docker compose down є безпечною — контейнери та мережа є тимчасовими, а ваші дані зберігаються у volume. docker compose down -v також видаляє іменовані volume. Це призведе до миттєвого видалення бази даних без підтвердження та можливості скасування. Прапор -v призначений для повного видалення експериментальних середовищ; у стеку з реальними даними ставтеся до нього так само, як до rm -rf. У /var/lib/docker/volumes/ немає кошика.
Для разового запуску shell всередині працюючого контейнера: docker compose exec db psql -U miniflux надає доступ до бази даних, а docker compose exec miniflux sh надає shell у додатку.
Де насправді зберігаються ваші дані
Іменовані volumes отримують префікс проєкту, тому 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 і копіюйте результат на інший сервер; резервна копія на тому ж диску, де зберігаються дані, є лише копією, а не бекапом. Nextcloud guide описує повний регламент запланованого резервного копіювання саме на основі цих двох підходів.
Режими помилок та відповідні повідомлення
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.
Далі guide по 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.