Назад к блогу
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, когда первый станет тесен

Что нужно

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

  • Ubuntu 22.04/24.04, root или sudo
  • Рекомендуется 2 ГБ ОЗУ (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

# Дальше файлы правьте уже под deploy:
# 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 (права 600)
POSTGRES_USER=app
POSTGRES_PASSWORD=change-me-long-random
POSTGRES_DB=app
# плюс APP_SECRET / NEXTAUTH_SECRET, если их ждёт образ

# /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

# По желанию в compose.yaml у app или db:
#    deploy:
#      resources:
#        limits:
#          memory: 512M

# Логи json-file не должны расти бесконечно:
# { "log-driver": "json-file", "log-opts": { "max-size": "10m", "max-file": "3" } }
# в /etc/docker/daemon.json, затем: systemctl restart docker

Шаг 6. Обновления и откат

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

bash
cd /srv/app
git pull   # если Compose лежит в git

# Смените тег образа в compose.yaml, затем:
docker compose pull
docker compose up -d
docker compose ps

# Откат: старый тег, pull, снова up -d
# НИКОГДА: docker compose down -v   # удаляет именованные тома

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

Снимок VPS полезен. Логический дамп Postgres лучше, если восстанавливать нужно на другую машину. Поставьте это на cron. Один раз восстановите копию, иначе у вас не копии, а файлы, на которые вы надеетесь.

bash
# Дамп, пока контейнер db запущен:
docker compose exec -T db pg_dump -U app app | gzip > /var/backups/app-$(date +%F).sql.gz

# Унесите с сервера
# scp /var/backups/app-*.sql.gz backup-host:~

# Набросок восстановления (окно работ):
# 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.