← Все статьи
Полезные статьи

Docker на VPS: несколько проектов на одной машине

Как держать несколько сайтов и сервисов на одном VPS через Docker: установка, docker compose, общий reverse proxy с автоматическим SSL, лимиты ресурсов и обновления.

Второй проект на сервере почти всегда начинается с конфликта. Первому сайту нужен 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 переносятся на новый сервер как есть.

Выбрать сервер под Docker →

Команда Puws.Cloud
Puws.Cloud
Начать

СЕРВЕР ДЛЯ ВАШЕГО ПРОЕКТА

Начните
с тарифа.

ПОЛЕЗНОЕ О СЕРВЕРАХИнструкции
и советы.
Открыть блог
Наш Аптайм