Как выкладывать приложения через 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, когда первый станет тесен
Что нужно
ОЗУ должно хватать на приложение плюс 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, чтобы приходили обновления безопасности.
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
# Дальше файлы правьте уже под deploy:
# 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 (права 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.
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 удаляет тома — это стирание, а не перезапуск.
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. Один раз восстановите копию, иначе у вас не копии, а файлы, на которые вы надеетесь.
# Дамп, пока контейнер 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.