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

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

DonVardixDonVardix—

Введение

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

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

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

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


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

Запуск веб-контейнеров и сервисов напрямую под суперпользователем root небезопасен. Поэтому под root мы выполняем только первичную инициализацию — создаём системного пользователя deploy, добавляем его в группы sudo и docker и выделяем ему рабочие директории. Вся дальнейшая настройка и деплой сайтов выполняются строго от имени deploy.

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

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

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

1.2. Настройка SSH-доступа и безопасности

Чтобы подключаться к серверу напрямую под пользователем deploy без пароля и защитить систему от брутфорс-атак, настроим вход по ключу и отключим небезопасные методы аутентификации.

1. Добавление публичного ключа

На сервере под пользователем root подготовьте директорию и файл авторизованных ключей:

mkdir -p /home/deploy/.ssh
chmod 700 /home/deploy/.ssh
nano /home/deploy/.ssh/authorized_keys

Вставьте публичный ключ вашего локального компьютера (содержимое локального файла ~/.ssh/id_ed25519.pub). Если у вас ещё не создан SSH-ключ, обратитесь к подробному руководству Создание SSH-ключей и их настройка.

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

chmod 600 /home/deploy/.ssh/authorized_keys
chown -R deploy:deploy /home/deploy/.ssh

2. Проверка входа по ключу

В новом окне терминала на вашем локальном компьютере проверьте прямое подключение:

ssh deploy@<ip_сервера>

Убедитесь, что вход прошёл успешно без запроса пароля, а команда sudo whoami отвечает root.

3. Отключение паролей и входа под root

После подтверждения успешного входа под deploy заблокируйте вход по паролю и прямой вход суперпользователя root. Создайте файл конфигурации:

sudo nano /etc/ssh/sshd_config.d/99-security.conf
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes

Проверьте конфигурацию на ошибки синтаксиса и примените настройки:

sudo sshd -t
sudo systemctl reload ssh

Безопасность SSH

Не закрывайте текущую сессию терминала, пока в новом окне не убедитесь, что команда ssh deploy@<ip_сервера> успешно подключается без пароля.

4. Быстрый доступ через локальный SSH config

На своем локальном компьютере добавьте запись в ~/.ssh/config:

Host my-vps
    HostName <ip_сервера>
    User deploy
    IdentityFile ~/.ssh/id_ed25519

Теперь подключаться к серверу можно короткой командой: ssh my-vps.

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

Для порядка и изоляции разделим инфраструктуру прокси и сами сайты на две независимые папки:

  • /var/www/proxy — директория для центрального реверс-прокси Caddy (здесь размещаются его docker-compose.yml и конфигурация Caddyfile);
  • /var/www/apps — каталог для проектов (каждый сайт или сервис живёт в своей изолированной подпапке).

Под пользователем deploy создайте структуру каталогов и назначьте его владельцем всей ветки /var/www:

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

Передача прав (chown) гарантирует, что все последующие операции — создание файлов, git clone, сборка образов и запуск контейнеров — будут выполняться пользователем deploy напрямую, без вызова sudo и без конфликта прав доступа к логам и кэшу.


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

2.1. Создание общей Docker-сети

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

docker network create web_gateway

2.2. Создание конфигурации 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
}

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

Запустите 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

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

Чтобы сервер мог клонировать приватные репозитории и обновлять код проектов командой git pull при деплое (как вручную, так и автоматически через пайплайны GitHub Actions), настроим сервисный SSH-ключ. Это избавит от необходимости ввода логина/пароля при обращениях к Git и защитит основной аккаунт разработчика.

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

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 (или в настройки конкретного репозитория как Deploy Key), после чего проверьте соединение:

ssh -T git@github.com

Ответ с приветствием Hi ...! You've successfully authenticated... подтверждает готовность сервера к загрузке исходного кода по SSH.


4. Создание и запуск нового сайта

После подготовки сервера добавление нового проекта выполняется по единому трёхшаговому алгоритму.

4.1. Настройка DNS и SSL в Cloudflare

Перед запуском проекта направьте домен на IP-адрес вашего сервера и задайте параметры шифрования в панели Cloudflare:

  1. Создание A-записи (DNS → Records):

    • Name: имя домена или поддомена (mysite.example.com или @);
    • IPv4 address: публичный IP-адрес вашего VPS (<ip_сервера>);
    • Proxy status: Proxied (оранжевое облако).
  2. Режим шифрования (SSL/TLS → Overview): установите режим Full (strict).
    (Режим Flexible вызывает бесконечный редирект 301 между Cloudflare и Caddy).

  3. HTTPS и перенаправления:

    • SSL/TLS → Edge Certificates: включите Always Use HTTPS;
    • 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
}

4.2. Развертывание контейнера проекта

Создайте директорию приложения в /var/www/apps, добавьте простой тестовый скрипт и запустите контейнер на базе FrankenPHP. Главное требование к docker-compose.yml любого проекта — подключение к внешней сети web_gateway и отсутствие опубликованных наружу портов (ports):

mkdir -p /var/www/apps/my-app
cd /var/www/apps/my-app
echo "<?php phpinfo();" > index.php
nano docker-compose.yml
services:
  app:
    image: dunglas/frankenphp:php8.3-alpine
    container_name: myapp
    restart: unless-stopped
    volumes:
      - .:/app
    networks:
      - web_gateway

networks:
  web_gateway:
    external: true

Запустите проект:

docker compose up -d

4.3. Маршрутизация в Caddy и выпуск SSL

Откройте конфигурацию реверс-прокси:

nano /var/www/proxy/Caddyfile

Добавьте блок сайта с указанием имени контейнера приложения и его внутреннего порта:

mysite.example.com {
    reverse_proxy myapp:80
}

Примените изменения без остановки Caddy и соседних контейнеров:

docker compose -f /var/www/proxy/docker-compose.yml exec caddy caddy reload --config /etc/caddy/Caddyfile

Caddy автоматически получит SSL-сертификат от Let’s Encrypt / ZeroSSL, и сайт сразу станет доступен по HTTPS.


Безопасность и оптимизация сервера

Для стабильной работы продакшена необходимо защитить сервер от переполнения диска, нехватки оперативной памяти и нежелательного сетевого трафика.

Настройка фаервола (UFW)

Закройте все входящие соединения, оставив только порты SSH, HTTP и HTTPS (включая UDP для поддержки HTTP/3):

sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow 443/udp
sudo ufw enable

Так как контейнеры сайтов не публикуют порты наружу (ports отсутствуют) и общаются с Caddy через изолированную сеть web_gateway, базы данных и служебные порты остаются полностью закрытыми от внешнего мира.

Ротация логов Docker

По умолчанию Docker не ограничивает размер логов контейнеров, что со временем приводит к 100% заполнению диска и аварийной остановке всех сервисов. Ограничьте размер логов глобально в /etc/docker/daemon.json:

sudo nano /etc/docker/daemon.json
{
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "10m",
    "max-file": "3"
  }
}

Перезапустите демон Docker для применения настроек:

sudo systemctl restart docker

Теперь каждый контейнер хранит максимум 30 МБ логов.

Файл подкачки (Swap) и защита от OOM

При кратковременных всплесках нагрузки механизм ядра Linux (OOM Killer) может принудительно завершить процесс базы данных или Caddy при нехватке памяти. Для защиты от сбоев создайте swap-файл на 2 ГБ с приоритетом использования оперативной памяти (vm.swappiness=10):

sudo fallocate -l 2G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
echo 'vm.swappiness=10' | sudo tee -a /etc/sysctl.conf
sudo sysctl -p

Автоматические обновления безопасности

Настройте автоматическую установку критических исправлений безопасности ядра и системных пакетов без необходимости ручного обновления:

sudo apt update && sudo apt install -y unattended-upgrades
sudo dpkg-reconfigure --priority=low unattended-upgrades

Шаблоны проектов (FrankenPHP)

Вместо устаревшей и громоздкой связки Nginx + PHP-FPM с двумя отдельными контейнерами современный стандарт — использование FrankenPHP. Это сверхбыстрый сервер приложений на базе Caddy, который запускает PHP-код и отдаёт статику внутри одного легковесного контейнера.

Шаблон: Чистый PHP

Готовая конфигурация для легковесных скриптов и проектов на чистом PHP.

В корне проекта создайте Dockerfile для добавления необходимых PHP-расширений (утилита install-php-extensions устанавливает любые модули в одну команду):

FROM dunglas/frankenphp:php8.3-alpine

# Установка расширений (при необходимости)
RUN install-php-extensions pdo_mysql opcache

COPY . /app

Файл docker-compose.yml в папке /var/www/apps/my-app/docker-compose.yml:

services:
  app:
    build: .
    container_name: pure_php_app
    restart: unless-stopped
    volumes:
      - .:/app
    networks:
      - web_gateway

networks:
  web_gateway:
    external: true

В /var/www/proxy/Caddyfile проксируйте домен на порт 80 контейнера:

pure-php.example.com {
    reverse_proxy pure_php_app:80
}

Шаблон: Laravel

Для приложений на Laravel настраивается точка входа в папку public, права на запись в storage и база данных MariaDB во внутренней сети.

Файл Dockerfile в корне Laravel-проекта:

FROM dunglas/frankenphp:php8.3-alpine

# Установка стандартных расширений для Laravel
RUN install-php-extensions \
    pdo_mysql \
    redis \
    bcmath \
    intl \
    opcache

WORKDIR /app

COPY . /app

# Назначение прав на папки кэша и сессий пользователю deploy (UID 1000)
RUN chown -R 1000:1000 /app/storage /app/bootstrap/cache

Файл docker-compose.yml в папке /var/www/apps/laravel-app/docker-compose.yml:

services:
  app:
    build: .
    container_name: laravel_app
    restart: unless-stopped
    environment:
      SERVER_NAME: ":80"
      CADDY_GLOBAL_OPTIONS: "auto_https off"
      FRANKENPHP_CONFIG: "root /app/public"
    volumes:
      - .:/app
    networks:
      - web_gateway
      - internal
    depends_on:
      db:
        condition: service_healthy

  db:
    image: mariadb:11.4
    container_name: laravel_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:
  db_data:

В /var/www/proxy/Caddyfile:

laravel.example.com {
    reverse_proxy laravel_app:80
}

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

Если проект больше не нужен, удалите его без остановки 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 сервера.


Советы и практики

Соблюдение нескольких правил обеспечит стабильную работу мультипроектного стека и упростит его повседневное обслуживание.

Обновление и деплой

Для ручного обновления существующего проекта перейдите в его директорию и перезапустите контейнеры:

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

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

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

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

  • Просмотр логов Caddy:
    docker compose -f /var/www/proxy/docker-compose.yml logs -f --tail=50 caddy
  • Диагностика ошибки 502 (Bad Gateway):
    docker compose -f /var/www/proxy/docker-compose.yml logs --tail=20 caddy
    (Ошибка connection refused означает, что контейнер приложения не запущен или слушает другой внутренний порт).
  • Мониторинг ресурсов контейнеров:
    docker stats
  • Проверка подключенных к шлюзу контейнеров:
    docker network inspect web_gateway
  • Перезапуск конкретного проекта:
    cd /var/www/apps/<проект> && docker compose restart