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.