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

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, щоб приходили оновлення безпеки.
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 не повинен бути доступний усім на запис.
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 — джерело раптових поломок.
# /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.
# /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.
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 видаляє томи — це стирання, а не перезапуск.
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. Один раз відновіть копію, інакше у вас не копії, а файли, на які ви сподіваєтесь.
# 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.