Ссылка скопирована

Настройка VPS под мультипроектный Docker-стек с Caddy и Cloudflare

DonVardix DonVardix

Введение

Эта инструкция — пошаговый воспроизводимый шаблон настройки чистого VPS (Ubuntu) для одновременной работы нескольких изолированных проектов.

Суть архитектуры:

  • Наружу открыты только порты 80 и 443.
  • Единственная точка входа — легковесный реверс-прокси Caddy, который берет на себя маршрутизацию и автоматический выпуск SSL-сертификатов.
  • Проекты изолированы в своих docker-compose файлах и общаются с Caddy через единую виртуальную сеть web_gateway. Базы данных и служебные сервисы наружу не светятся.
  • Все операции выполняются под выделенным пользователем deploy с правами 1000:1000.

Следуя шагам ниже, вы получите готовую к продакшену инфраструктуру, куда новые сайты добавляются за пару минут.


1. Пользователь deploy и директории

Все действия на сервере выполняются под пользователем deploy, а не root.

1.1. Создание пользователя

Под пользователем root:

adduser deploy
usermod -aG sudo deploy
usermod -aG docker deploy

Переключитесь на пользователя deploy:

su - deploy

1.2. Создание структуры папок

sudo mkdir -p /var/www/proxy /var/www/apps
sudo chown -R deploy:deploy /var/www

Важно

Все последующие команды, файлы и git-клонирования выполняются строго под пользователем deploy без sudo.

2. Общая Docker-сеть

Создайте внешнюю сеть-мост для связи Caddy с веб-контейнерами проектов:

docker network create web_gateway

3. Центральный реверс-прокси Caddy

3.1. Создание конфигурации Caddy

Перейдите в директорию прокси:

cd /var/www/proxy
nano docker-compose.yml

Содержимое docker-compose.yml:

services:
  caddy:
    image: caddy:2-alpine
    container_name: caddy_proxy
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
      - "443:443/udp"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - caddy_data:/data
      - caddy_config:/config
    networks:
      - web_gateway

networks:
  web_gateway:
    external: true

volumes:
  caddy_data:
  caddy_config:

Создайте Caddyfile:

nano /var/www/proxy/Caddyfile
{
    email admin@example.com
}

3.2. Запуск и перезагрузка

Запустите Caddy:

docker compose -f /var/www/proxy/docker-compose.yml up -d

Команда перезагрузки

Для применения изменений в Caddyfile всегда явно указывайте путь к конфигу: docker compose -f /var/www/proxy/docker-compose.yml exec caddy caddy reload --config /etc/caddy/Caddyfile

4. Настройка Cloudflare

Для корректной работы SSL и защиты трафика выставьте настройки в панели Cloudflare:

  1. SSL/TLS -> Overview: установите режим Full (strict).
    (Режим Flexible вызывает бесконечный редирект 301 между Cloudflare и Caddy).
  2. SSL/TLS -> Edge Certificates: включите Always Use HTTPS.
  3. Rules -> Redirect Rules: создайте правило для перенаправления с www.* на корень https://${1}.
  4. Поддомены 4-го уровня (например, www.app.example.com):
    Бесплатный Universal SSL от Cloudflare не покрывает домены глубже 3-го уровня. Для таких записей в DNS выставляйте статус DNS only (серое облако), а в Caddyfile добавьте редирект:
www.app.example.com {
    redir https://app.example.com{uri} permanent
}

5. SSH-доступ к GitHub

Под пользователем deploy сгенерируйте ключ для доступа к репозиториям:

ssh-keygen -t ed25519 -C "deploy" -f ~/.ssh/github_deploy

Создайте ~/.ssh/config:

Host github.com
    HostName github.com
    User git
    IdentityFile ~/.ssh/github_deploy
    IdentitiesOnly yes

Выставьте права и проверьте соединение:

chmod 600 ~/.ssh/config ~/.ssh/github_deploy
chmod 644 ~/.ssh/github_deploy.pub
cat ~/.ssh/github_deploy.pub

Скопируйте публичный ключ в GitHub -> Settings -> SSH Keys и выполните проверку:

ssh -T git@github.com

6. Шаблон проекта: Laravel / PHP + Nginx

При развертывании классического стека важно учесть 3 момента:

  1. Контейнеры не публикуют порты наружу (ports: отсутствуют).
  2. Веб-сервер web подключен к сетям web_gateway и внутренней internal. Служебные сервисы (app, db, redis) доступны только в internal.
  3. В официальном образе nginx:alpine обязательно задается переменная NGINX_ENVSUBST_FILTER, иначе утилита шаблонизации сотрет системные переменные Nginx ($uri, $fastcgi_script_name) и вызовет ошибку 500.

6.1. Пример docker-compose.yml проекта

В папке /var/www/apps/my-app/docker-compose.yml:

services:
  web:
    image: nginx:alpine
    container_name: myapp_web
    restart: unless-stopped
    environment:
      PROJECT_NAME: myapp
      NGINX_ENVSUBST_FILTER: PROJECT_NAME
    volumes:
      - ./docker/nginx/templates:/etc/nginx/templates:ro
      - ./public:/var/www/html/public:ro
      - storage_data:/var/www/html/storage:ro
    networks:
      - internal
      - web_gateway
    depends_on:
      - app

  app:
    build:
      context: .
      dockerfile: Dockerfile
    container_name: myapp_app
    restart: unless-stopped
    volumes:
      - .:/var/www/html
      - storage_data:/var/www/html/storage
    networks:
      - internal
    depends_on:
      db:
        condition: service_healthy

  db:
    image: mariadb:11.4
    container_name: myapp_db
    restart: unless-stopped
    environment:
      MYSQL_ROOT_PASSWORD: rootsecret
      MYSQL_DATABASE: myapp
      MYSQL_USER: myapp
      MYSQL_PASSWORD: secret
    volumes:
      - db_data:/var/lib/mysql
    networks:
      - internal
    healthcheck:
      test: ["CMD-SHELL", "mariadb-admin ping -h localhost -uroot -p$$MYSQL_ROOT_PASSWORD || exit 1"]
      interval: 10s
      timeout: 5s
      retries: 5

networks:
  web_gateway:
    external: true
  internal:
    driver: bridge

volumes:
  storage_data:
  db_data:

6.2. Нюансы прав в Dockerfile

Чтобы PHP-FPM мог беспрепятственно писать в кэш и логи, согласуйте UID системного пользователя www-data с пользователем deploy (1000):

ARG UID=1000
ARG GID=1000

RUN apk add --no-cache shadow \
    && usermod -u ${UID} www-data \
    && groupmod -g ${GID} www-data

7. Шаблон составного проекта (Next.js + Fastify API)

Если проект состоит из раздельных контейнеров фронтенда и API:

  1. Оба контейнера включаются в сеть web_gateway и получают фиксированные имена.
  2. В /var/www/proxy/Caddyfile маршруты разделяются через директивы handle:
api-app.example.com {
    handle /api/* {
        reverse_proxy app_backend:3001
    }

    handle {
        reverse_proxy app_frontend:3000
    }
}

502 Bad Gateway

Если Caddy возвращает 502, проверьте внутренний порт сервиса: docker compose -f /var/www/proxy/docker-compose.yml logs --tail=20 caddy. Ошибка connection refused означает, что приложение слушает другой порт внутри своего контейнера.

8. Удаление сайта с сервера

Если проект больше не нужен, удалите его без остановки Caddy и без влияния на соседние работающие сайты:

  1. Остановка контейнеров и удаление данных:
    Остановите контейнеры проекта с флагом -v, чтобы гарантированно стереть связанные Docker Volumes (базу данных, кэш и постоянное хранилище):

    cd /var/www/apps/<проект>
    docker compose down -v
  2. Удаление файлов с диска:
    Удалите директорию проекта:

    cd /var/www/apps
    rm -rf <проект>
  3. Отключение маршрутизации в Caddy:
    Откройте /var/www/proxy/Caddyfile:

    nano /var/www/proxy/Caddyfile

    Удалите блок с доменом этого сайта и примените обновленную конфигурацию на лету:

    docker compose -f /var/www/proxy/docker-compose.yml exec caddy caddy reload --config /etc/caddy/Caddyfile
  4. Очистка неиспользуемых Docker-образов:
    Освободите дисковое пространство от оставшихся образов проекта:

    docker image prune -af
  5. Удаление DNS-записи:
    Удалите соответствующую A-запись домена в панели управления Cloudflare, чтобы входящий трафик не отправлялся на IP сервера.


Вывод

Вся инфраструктура настроена и работает как единый шлюз. Ниже алгоритм повседневной эксплуатации сервера.

1. Добавление нового сайта за 3 шага

  1. DNS: Создайте A-запись домена в Cloudflare на IP вашего VPS (оранжевое облако).
  2. Запуск контейнера: В папке проекта (/var/www/apps/<проект>) укажите в docker-compose.yml сеть web_gateway и имя веб-контейнера:
    cd /var/www/apps/<проект>
    docker compose up -d
  3. Маршрутизация в Caddy: Добавьте сайт в /var/www/proxy/Caddyfile:
    newsite.com {
        reverse_proxy <имя_веб_контейнера>:80
    }
    И примените конфигурацию:
    docker compose -f /var/www/proxy/docker-compose.yml exec caddy caddy reload --config /etc/caddy/Caddyfile
    Caddy мгновенно выпустит SSL-сертификат и начнет проксировать трафик.

2. Обновление существующего проекта (деплой)

В директории проекта:

cd /var/www/apps/<проект>
git pull
docker compose build
docker compose up -d

Сайт обновится без простоя и без необходимости трогать Caddy или соседние сайты.

Для настройки непрерывного автоматического обновления проектов при git push без ручного ввода команд используйте руководство Автодеплой Docker-проектов на VPS через GitHub Actions и SSH.

3. Шпаргалка команд

  • Просмотр логов Caddy:
    docker compose -f /var/www/proxy/docker-compose.yml logs -f --tail=50 caddy
  • Проверка подключенных к шлюзу контейнеров:
    docker network inspect web_gateway
  • Перезапуск конкретного проекта:
    cd /var/www/apps/<проект> && docker compose restart