Второй проект на сервере почти всегда начинается с конфликта. Первому сайту нужен PHP 7.4, второму — 8.3. Один проект работает на Node 18, другой на 22. Приложение требует старую версию библиотеки, и после её установки ломается соседнее.
Docker решает это тем, что каждый проект получает собственное окружение со своими версиями — на одном сервере, но не пересекаясь с соседями. Плюс приятный побочный эффект: проект переезжает на другой сервер копированием одной папки.
Разбираем, как поднять на VPS несколько проектов с доменами и SSL.
Сколько ресурсов нужно
Сам Docker почти ничего не занимает — накладные расходы измеряются десятками мегабайт. Память тратят приложения внутри контейнеров, ровно столько же, сколько они тратили бы без контейнеров.
| Что разворачиваем | Подходящий тариф |
|---|---|
| 2–3 лёгких сервиса (бот, статика, небольшой API) | Старт — 1 ядро, 2 ГБ |
| Сайт + база + кэш | Лёгкий — 2 ядра, 4 ГБ |
| Несколько полноценных проектов с базами | Оптимальный — 4 ядра, 8 ГБ |
| Сборка образов на сервере, CI, тяжёлые стеки | Бизнес — 8 ядер, 16 ГБ |
Отдельно про диск: образы занимают место, и 10 ГБ на тарифе «Старт» кончаются быстрее, чем кажется. Раз в месяц чистите неиспользуемое — команда есть ниже.
Как считать ресурсы под конкретную нагрузку: Какой VPS выбрать.
Шаг 1. Установить Docker
Перед установкой пройдите базовую настройку сервера — отдельный пользователь, ключи, firewall: 7 шагов безопасности.
Установка одной командой с официального скрипта:
curl -fsSL https://get.docker.com | sh
Разрешите своему пользователю работать с Docker без sudo:
sudo usermod -aG docker $USER
Изменение вступит в силу после нового входа — отключитесь и подключитесь заново.
Проверка:
docker run --rm hello-world
Вывод «Hello from Docker!» означает, что всё работает. Плагин docker compose ставится вместе с Docker, проверить:
docker compose version
Шаг 2. Разложить проекты по папкам
Структура, которая не путается при десятке проектов:
/opt/
proxy/ — общий reverse proxy
docker-compose.yml
Caddyfile
shop/ — первый проект
docker-compose.yml
blog/ — второй проект
docker-compose.yml
Каждый проект — отдельная папка со своим docker-compose.yml. Управляется независимо: можно перезапустить один, не трогая остальные.
Шаг 3. Поднять общий reverse proxy
Порт 80 на сервере один, а сайтов несколько. Нужен «диспетчер», который смотрит на домен в запросе и направляет его в нужный контейнер.
Удобнее всего для этого Caddy: он умеет сам выпускать и продлевать SSL-сертификаты Let's Encrypt, без certbot и cron.
Создайте общую сеть, через которую proxy будет видеть проекты:
docker network create web
mkdir -p /opt/proxy && cd /opt/proxy
nano docker-compose.yml
services:
caddy:
image: caddy:2
container_name: caddy
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
- caddy_data:/data
networks:
- web
volumes:
caddy_data:
networks:
web:
external: true
Том caddy_data хранит выпущенные сертификаты — без него они будут теряться при каждом перезапуске, а у Let's Encrypt есть лимит на количество выпусков.
Теперь сам конфиг маршрутизации:
nano Caddyfile
shop.example.com {
reverse_proxy shop-app:80
}
blog.example.com {
reverse_proxy blog-app:3000
}
Здесь shop-app и blog-app — имена контейнеров, а не адреса. Внутри общей сети Docker сам разрешает имена в адреса.
Запуск:
docker compose up -d
Перед запуском убедитесь, что A-записи обоих доменов уже указывают на IP сервера — Caddy выпускает сертификаты сразу при старте, и без правильных DNS они не выпустятся.
Шаг 4. Добавить первый проект
Пример сайта на PHP с базой:
mkdir -p /opt/shop && cd /opt/shop
nano docker-compose.yml
services:
shop-app:
image: php:8.3-apache
container_name: shop-app
restart: unless-stopped
volumes:
- ./src:/var/www/html
networks:
- web
- internal
depends_on:
- shop-db
shop-db:
image: mysql:8
container_name: shop-db
restart: unless-stopped
environment:
MYSQL_DATABASE: shop
MYSQL_USER: shop
MYSQL_PASSWORD: ${DB_PASSWORD}
MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
volumes:
- shop_db:/var/lib/mysql
networks:
- internal
volumes:
shop_db:
networks:
web:
external: true
internal:
Обратите внимание на сети. Приложение подключено к двум: web — чтобы его видел proxy, internal — чтобы оно видело базу. Сама база в сеть web не входит, поэтому снаружи к ней не достучаться. У неё вообще нет проброшенных портов — и это правильно: база должна быть доступна только своему приложению.
Пароли держим в отдельном файле, а не в compose:
nano .env
DB_PASSWORD=длинный_пароль
DB_ROOT_PASSWORD=другой_длинный_пароль
chmod 600 .env
Запуск:
docker compose up -d
Через полминуты https://shop.example.com открывается с рабочим сертификатом.
Второй проект добавляется точно так же: своя папка, свой compose, новый блок в Caddyfile. После правки конфига proxy его нужно перечитать:
docker exec caddy caddy reload --config /etc/caddy/Caddyfile
Шаг 5. Ограничить аппетиты контейнеров
Без лимитов одно приложение с утечкой памяти способно положить весь сервер — система начнёт убивать процессы, и под раздачу попадут соседние проекты.
Добавьте в описание сервиса:
deploy:
resources:
limits:
memory: 1g
cpus: "1.0"
Контейнер не возьмёт больше гигабайта памяти и одного ядра. Если упрётся в лимит по памяти — будет перезапущен, но остальные проекты продолжат работать.
Смотреть текущее потребление:
docker stats
Повседневные команды
docker ps # что сейчас работает
docker compose logs -f # логи проекта в реальном времени
docker compose logs --tail 100 # последние 100 строк
docker compose restart # перезапустить проект
docker compose down # остановить и удалить контейнеры
docker compose up -d # поднять заново
Команды compose выполняются из папки проекта.
Обновление образов до свежих версий:
cd /opt/shop
docker compose pull
docker compose up -d
Docker пересоздаст только те контейнеры, чей образ обновился. Данные в томах при этом не теряются.
Зайти внутрь контейнера, чтобы выполнить команду:
docker exec -it shop-app bash
Чистка диска
Старые образы и слои накапливаются незаметно. Посмотреть, сколько занято:
docker system df
Удалить всё неиспользуемое:
docker system prune -a
Эта команда удаляет образы, которые не используются работающими контейнерами. Тома с данными она не трогает — если только не добавить
--volumes. Этот флаг без крайней необходимости лучше не использовать: он снесёт базы.
Хорошая привычка — прогонять prune раз в месяц, особенно на тарифах с небольшим диском.
Бэкапы контейнеров
Резервная копия проекта на Docker — это две вещи: папка с docker-compose.yml и данные из томов. Сами образы бэкапить не нужно, они скачиваются заново.
Копия базы из контейнера:
docker exec shop-db mysqldump -u root -p"$DB_ROOT_PASSWORD" shop > /opt/backups/shop-$(date +%F).sql
Копия тома целиком:
docker run --rm -v shop_db:/data -v /opt/backups:/backup alpine tar -czf /backup/shop_db-$(date +%F).tar.gz /data
Как поставить это на автомат и хранить копии вне сервера: Бэкапы VPS за 10 минут.
Когда Docker не нужен
Не стоит тащить контейнеры туда, где они только усложняют.
Один сайт на одном сервере, который вы не планируете переносить, прекрасно живёт на обычном nginx + PHP-FPM. Простой Telegram-бот проще запустить через systemd — там всего один процесс и один файл службы: Telegram-бот 24/7 на сервере.
Docker окупается, когда проектов больше одного, когда у них разные версии окружения или когда проект нужно повторяемо разворачивать на разных машинах.
Частые вопросы
Docker замедляет работу? Контейнеры используют ядро хоста напрямую, виртуализации внутри нет. Потери производительности — в пределах процентов, и на практике незаметны. Заметно медленнее может быть дисковый ввод-вывод при монтировании больших папок, но для типовых задач это не критично.
Нужен ли nginx на хосте, если есть Caddy в контейнере? Нет, и ставить его не стоит: он займёт порт 80, и Caddy не запустится. Диспетчер должен быть один.
Как добавить третий, четвёртый проект?
Новая папка, свой docker-compose.yml, подключение к сети web, новый блок в Caddyfile, reload. Порядок один и тот же для любого количества проектов.
Что будет с контейнерами после перезагрузки сервера?
Со строкой restart: unless-stopped они поднимутся автоматически. Без неё — останутся остановленными.
Можно ли запускать Docker на тарифе «Старт»? Да. Ограничение не в Docker, а в 2 ГБ памяти и 10 ГБ диска: два-три лёгких сервиса помещаются, полноценный магазин с базой — уже тесно.
Чем docker compose отличается от docker-compose?
Это старая и новая версии. Сейчас используется docker compose (через пробел) — она ставится вместе с Docker. Инструкции со старым написанием через дефис устарели.
Итог
Схема простая: один общий reverse proxy на портах 80 и 443, рядом — папки проектов, каждый в своей изоляции. Добавление нового проекта занимает пару минут и ничем не грозит уже работающим.
Начать можно с тарифа «Старт» за 5 € и пары сервисов, а когда проектов станет больше — перейти на конфигурацию выше без переустановки: папки с compose переносятся на новый сервер как есть.

