Назад до блогу
Серпень 19, 2026Посібники

Як розгортати програми через Docker Compose на VPS

Практичний посібник для продакшену з Docker Compose v2 на Ubuntu: встановлення рушія, файл Compose з мережами й томами, секрети, зворотний проксі, оновлення без паніки та резервні копії іменованих томів.

Як розгортати програми через Docker Compose на VPS

Docker Compose — звичний спосіб запустити невеликий стек на одному VPS: вебзастосунок, база, кеш Redis і зворотний проксі в одному YAML. Це не Kubernetes і не відмовостійкість на кілька машин. Зате схема повторювана: її можна підняти на новому VPS Hiddence за хвилини. Саме це потрібно більшості побічних проєктів і невеликих продуктів.

Стаття про варіант ближчий до бойового, а не про docker run у п’ятницю ввечері. Поставимо Docker Engine і плагін Compose, заведемо користувача для викладки без root у повсякденні, напишемо Compose із перевірками живості та політикою перезапуску, приберемо секрети з Git, назовні опублікуємо лише порти проксі й опишемо оновлення та копії. Приклад — вебзастосунок, PostgreSQL і Caddy; той самий каркас підходить для Node, PHP, Python або набору воркерів.

Чому Compose на одному VPS усе ще доречний

У Kubernetes йдуть, бо так прийнято, а потім проводять вихідні над YAML для одного сайту. Compose читається. Файл можна версіонувати, змінні описати, зміну подивитися diff’ом до застосування. Ізоляції вистачає: скомпрометований контейнер застосунку не повинен тримати сокет бази, якщо мережі розділені й права мінімальні. Ліміти пам’яті не дають одній витоку покласти весь VPS.

  • Весь стек описаний одним файлом
  • Іменовані томи переживають перестворення контейнерів
  • Внутрішні мережі Docker не виставляють Postgres в інтернет
  • restart: unless-stopped закриває більшість перезавантажень
  • Легко скопіювати на другий VPS, коли перший стане тісним

Що потрібно

RAM має вистачати на застосунок плюс Postgres. Реалістичний мінімум — тариф на 2 ГБ, якщо є база й проксі. Нижче — Ubuntu 24.04. Не ставте Docker із випадкового Snap, не розібравшись із cgroups; кроки використовують офіційний репозиторій Docker.

  • Ubuntu 22.04/24.04, root або sudo
  • Рекомендовано 2 ГБ RAM (1 ГБ лише для крихітних стеків без Postgres)
  • Домен на IP VPS, якщо потрібен автоматичний HTTPS
  • Git або інший спосіб доставити проєкт на сервер

Крок 1. Встановіть Docker Engine і Compose v2

Compose v2 — плагін, команда docker compose (з пробілом), а не старий двійковий файл docker-compose на Python. Ставте обидва компоненти з репозиторію Docker, щоб приходили оновлення безпеки.

bash
ssh root@YOUR_VPS_IP
apt update && apt -y install ca-certificates curl gnupg
install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
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" | tee /etc/apt/sources.list.d/docker.list > /dev/null

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

docker version
docker compose version

Крок 2. Користувач для викладки й каталоги

Compose від root працює, але окремий користувач у групі docker акуратніший. Проєкт кладіть у /opt або /srv, не в /root — так простіше з копіями й правами. Каталог із .env не повинен бути доступний усім на запис.

bash
adduser --disabled-password --gecos '' deploy
usermod -aG docker deploy
mkdir -p /srv/app
chown deploy:deploy /srv/app
chmod 750 /srv/app

# Log in as deploy for the rest of the file editing:
# su - deploy
# cd /srv/app

Крок 3. Compose-файл із заділом на бій

Нижче шаблон. Образ застосунку замініть своїм. Postgres не публікується на 0.0.0.0:5432 — назовні лише Caddy на 80 і 443. Перевірки живості не дають проксі слати трафік у застосунок, який ще мігрує базу. Теги образів фіксуйте: latest — джерело раптових поломок.

bash
# /srv/app/compose.yaml
services:
  caddy:
    image: caddy:2.8-alpine
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - caddy_data:/data
      - caddy_config:/config
    depends_on:
      app:
        condition: service_healthy

  app:
    image: ghcr.io/example/webapp:1.4.2
    restart: unless-stopped
    env_file: .env
    environment:
      DATABASE_URL: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
    depends_on:
      db:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "curl", "-fsS", "http://127.0.0.1:8000/health"]
      interval: 10s
      timeout: 3s
      retries: 10
    networks:
      - frontend
      - backend

  db:
    image: postgres:16-alpine
    restart: unless-stopped
    env_file: .env
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
      interval: 10s
      timeout: 5s
      retries: 10
    networks:
      - backend

networks:
  frontend: {}
  backend: {}

volumes:
  pgdata:
  caddy_data:
  caddy_config:

Крок 4. Секрети, .env і Caddyfile

Паролі — у .env на сервері, chmod 600, у Git не комітьте. Пароль: openssl rand -base64 32. У Caddyfile достатньо проксувати на ім’я служби застосунку в мережі Docker.

bash
# /srv/app/.env (permissions 600)
POSTGRES_USER=app
POSTGRES_PASSWORD=change-me-long-random
POSTGRES_DB=app
# plus any APP_SECRET / NEXTAUTH_SECRET your image needs

# /srv/app/Caddyfile
app.example.com {
    encode gzip
    reverse_proxy app:8000
}

chmod 600 /srv/app/.env
cd /srv/app
docker compose up -d
docker compose ps
docker compose logs -f --tail=100

Крок 5. Брандмауер, логи й ліміти пам’яті

В UFW залиште 22, 80 і 443. Docker іноді обходить UFW для опублікованих портів: якщо потрібен жорсткий фільтр на хості, вивчіть зв’язку Docker і UFW для вашої Ubuntu або публікуйте порти лише на 127.0.0.1 і тримайте Caddy на хості. У Compose задайте ліміти пам’яті, щоб Postgres не з’їв увесь VPS.

bash
ufw allow OpenSSH
ufw allow 80/tcp
ufw allow 443/tcp
ufw enable

# Optional in compose.yaml under app or db:
#    deploy:
#      resources:
#        limits:
#          memory: 512M

# Logs (do not let json-file grow forever):
# { "log-driver": "json-file", "log-opts": { "max-size": "10m", "max-file": "3" } }
# in /etc/docker/daemon.json then: systemctl restart docker

Крок 6. Оновлення та відкат

Нуднене оновлення, яке працює: завантажити нові образи, up -d, подивитися перевірки живості, старий тег тримати в git, щоб відкотитися. Не робіть docker compose down на бойовій базі, якщо не хочете зупинити прийом запитів. down -v видаляє томи — це стирання, а не перезапуск.

bash
cd /srv/app
git pull   # if the Compose file lives in git

# Change the image tag in compose.yaml, then:
docker compose pull
docker compose up -d
docker compose ps

# Rollback: set the old tag, pull, up -d again
# NEVER: docker compose down -v   # this deletes named volumes

Резервні копії іменованих томів

Знімок VPS корисний. Логічний дамп Postgres кращий, якщо відновлювати потрібно на іншу машину. Поставте це на cron. Один раз відновіть копію, інакше у вас не копії, а файли, на які ви сподіваєтесь.

bash
# Postgres dump while the db container is running:
docker compose exec -T db pg_dump -U app app | gzip > /var/backups/app-$(date +%F).sql.gz

# Copy off-box
# scp /var/backups/app-*.sql.gz backup-host:~

# Restore sketch (maintenance window):
# gunzip -c app-2026-08-19.sql.gz | docker compose exec -T db psql -U app app

Якщо щось не працює

Дивіться docker compose ps і docker compose logs служба. Образи не качаються — ліміти Docker Hub/ghcr або приватний образ без docker login. 502 у Caddy — застосунок не пройшов healthcheck або ім’я хоста проксі невірне (з контейнера Caddy це ім’я служби Compose, не localhost). Помилки прав на томах часто через uid 1000 в образі при каталозі root на хості.

  • compose: command not found — стоїть старе ім’я бінарника; потрібна команда docker compose
  • port is already allocated — 80/443 зайняті Apache, Nginx або іншим Caddy
  • database connection refused — застосунок стартанув раніше Postgres; healthcheck і depends_on з condition
  • Диск забитий — docker system df, потім обережний prune невикористовуваних образів
  • permission denied на docker.sock — користувач не в групі docker або не перелогінився

Захист

Порти баз не публікуйте. privileged: true не вмикайте. Сокет /var/run/docker.sock у контейнер застосунку не монтуйте, якщо ви не пишете менеджер контейнерів. Рушій оновлюйте. Свої образи має сенс сканувати. .env на диску — усе ще секрет: обмежте, хто може зайти по SSH.

  • Немає публічних 5432 / 3306 / 6379
  • Фіксуйте теги образів, краще навіть digest
  • chmod 600 у .env
  • Автооновлення пакетів ОС усе ще потрібні
  • Один проєкт Compose на застосунок зменшує радіус ураження

Поради

  • compose.yaml у git; .env.example без справжніх секретів
  • Профілі для необов’язкових воркерів (docker compose --profile workers up -d)
  • docker stats допомагає зрозуміти, який тариф VPS брати
  • Примонтувати вихідники як bind у бою — не той шаблон, збирайте образ
  • Коли стек роз’їдеться на кілька VPS, тоді й дивіться оркестрацію

Бойовий Compose на одному VPS — це офіційні пакети Docker, закритий .env, YAML без публікації бази, перевірки живості, проксі на 80/443, UFW, теги образів для відкату й дампи Postgres, які ви хоча б раз відновлювали. Візьміть шаблон зі статті, підставте свій образ і ставтеся до docker compose down -v як до rm -rf.