NexNode
Все гайды
ЭксплуатацияСредний

Деплой приложения через Docker Compose

Ставим Docker на чистый VPS, описываем сервисы в compose-файле, прячем их за nginx и настраиваем автозапуск.

11 мин чтенияобновлено 21 июля 2026 г.

Docker Compose удобен тем, что всё окружение приложения — сервисы, переменные, тома, сеть — описано одним файлом в репозитории. Развернуть проект на новом сервере становится вопросом одной команды.

Установка Docker

Пакет из репозитория Ubuntu обычно устаревший, поэтому ставим из официального репозитория:

curl -fsSL https://get.docker.com | sh

Проверяем, что всё встало, включая плагин compose:

docker --version
docker compose version

Чтобы запускать контейнеры без sudo, добавьте пользователя в группу docker (после этого нужно перезайти в систему):

usermod -aG docker $USER

Группа docker = права root

Любой в группе docker может смонтировать корень хоста внутрь контейнера и получить полный доступ к системе. Не добавляйте туда пользователей, которым не доверяете полностью.

Структура проекта

Минимальный набор для веб-приложения с базой:

/opt/myapp/
├── docker-compose.yml
├── .env
└── app/
    └── Dockerfile

Файл docker-compose.yml

services:
  app:
    build: ./app
    restart: unless-stopped
    env_file: .env
    ports:
      - "127.0.0.1:3000:3000"
    depends_on:
      db:
        condition: service_healthy

  db:
    image: postgres:16-alpine
    restart: unless-stopped
    environment:
      POSTGRES_USER: ${DB_USER}
      POSTGRES_PASSWORD: ${DB_PASSWORD}
      POSTGRES_DB: ${DB_NAME}
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${DB_USER}"]
      interval: 10s
      timeout: 5s
      retries: 5

volumes:
  pgdata:

Три детали, которые стоит заметить:

127.0.0.1:3000:3000 — порт публикуется только на localhost. Если написать просто 3000:3000, приложение окажется открыто всему интернету в обход nginx и файрвола, потому что Docker правит iptables сам.

restart: unless-stopped — контейнеры поднимутся после перезагрузки сервера, но останутся выключенными, если вы остановили их вручную.

healthcheck у базы — приложение стартует только после того, как Postgres реально готов принимать соединения, а не просто «запустился».

Переменные окружения

Секреты держим в .env рядом с compose-файлом и никогда не коммитим:

DB_USER=myapp
DB_PASSWORD=длинный-случайный-пароль
DB_NAME=myapp

Права на файл стоит закрыть:

chmod 600 .env

Запуск

cd /opt/myapp
docker compose up -d --build

Посмотреть, что происходит:

docker compose ps
docker compose logs -f app

Nginx как обратный прокси

Приложение слушает только localhost, наружу его отдаёт nginx — он же терминирует HTTPS:

server {
    listen 80;
    server_name app.example.ru;

    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }
}

Заголовки Upgrade и Connection нужны для веб-сокетов — без них живые обновления в приложении молча ломаются.

Дальше выпускаем сертификат по гайду про nginx и HTTPS.

Обновление версии

cd /opt/myapp
git pull
docker compose up -d --build
docker image prune -f

Последняя команда убирает старые слои образов — без неё диск незаметно забивается за несколько месяцев.

Резервные копии базы

Дамп прямо из контейнера:

docker compose exec -T db pg_dump -U myapp myapp | gzip > /opt/backups/myapp-$(date +%F).sql.gz

Добавьте в cron и обязательно проверьте восстановление — бэкап, который ни разу не разворачивали, бэкапом не считается.

Следите за местом на диске

Логи контейнеров по умолчанию не ротируются. Ограничьте их в /etc/docker/daemon.json: {"log-driver":"json-file","log-opts":{"max-size":"10m","max-file":"3"}} и перезапустите Docker.