Caddy: HTTPS, reverse proxy, PHP и Node.js — мастерская и примеры
Разбираем путь запроса от адреса сайта до приложения. Собираем Caddyfile в мастерской, запускаем PHP и Node.js за reverse proxy и проверяем HTTPS, маршруты и ответы сервера.

Caddy помогает открыть веб-приложение по понятному адресу, подключить HTTPS и направить запросы к нужному сервису. Один домен может показывать страницу, принимать форму в PHP, обращаться к Node.js API и передавать поток обновлений. Пользователь видит единый сайт, а разработчик управляет тем, какой компонент отвечает за каждый URL.
Здесь разберём устройство такого входа в приложение: что делает веб-сервер, зачем нужен reverse proxy, как появляется сертификат и почему правильный маршрут важнее случайно скопированного конфига. В мини-мастерской можно собрать Caddyfile и проследить изменение пути запроса. В архиве — настоящий стенд с Caddy, PHP-FPM и Node.js, который запускается через Docker Compose.
Что такое Caddy и какой результат он даёт
Представьте небольшой сервис: статическая страница лежит в одной папке, PHP принимает заявки, а Node.js выдаёт JSON для панели. Если показывать человеку порты каждого процесса, сервис быстро становится неудобным. Caddy принимает внешний запрос и выбирает дальнейшее действие. Файл он может отдать сам, PHP-скрипт передать PHP-FPM, а HTTP-запрос — приложению Node.js.
В роли веб-сервера Caddy обслуживает файлы и HTTP-ответы. В роли обратного прокси он находится перед приложениями и пересылает им запросы. Это один и тот же процесс с разными обработчиками. Конфигурация Caddyfile описывает адрес сайта, маршруты и дополнительные действия: сжатие, заголовки, журналы. Для обычного публичного домена Caddy также умеет автоматизировать получение и продление TLS-сертификата.
Практический результат — единая точка входа. Можно перенести внутреннее приложение на другой порт, сохранив внешний URL. Можно обслуживать страницу и API на одном origin. Можно видеть запросы к разным компонентам в общем журнале. При этом Caddy не заменяет код приложения: регистрация, проверка прав, работа с базой и бизнес-правила остаются задачей PHP или Node.js.
Официальная документация: Документация Caddy, Первый обратный прокси.
Где применять: сайт, API, кабинет и несколько приложений
| Ситуация | Роль Caddy | Что получает человек |
|---|---|---|
| Небольшой сайт или документация | Отдаёт HTML, CSS, изображения и архивы | Быстрая страница по одному адресу |
| PHP-приложение или CMS | Принимает HTTP и передаёт PHP в FastCGI | Формы, страницы и файлы одного сайта |
| Node.js API | Проксирует запросы к внутреннему HTTP-сервису | API через общий домен и HTTPS |
| SPA и серверный API | Разделяет пути интерфейса и данных | Обновление страницы не ломает вложенный маршрут |
| Личный кабинет с обновлениями | Проксирует поток SSE или WebSocket | Статус меняется без постоянной перезагрузки |
| Несколько небольших сервисов | Выбирает обработчик по домену или пути | Единый вход с понятной структурой URL |
Caddy особенно удобен, когда нужно быстро организовать понятный вход к нескольким частям проекта и поддерживать короткую конфигурацию. Например, основной сайт работает на PHP, а новый отчётный API написан на Node.js. Необязательно переносить весь сайт на другой стек: можно согласовать маршруты и постепенно подключить отдельный сервис.
Выбор прокси не снимает вопросов эксплуатации. Если у команды уже есть управляемая конфигурация другого сервера, наблюдение и проверенный процесс обновления, переход оценивают по конкретной пользе. Несколько коротких строк сами по себе не являются причиной миграции. Важнее, насколько удобно вашей команде проверять конфиг, понимать ошибки и восстанавливать работу.
Как проходит запрос: браузер, Caddy и backend
В обычной схеме браузер подключается к Caddy, а Caddy — к приложению. Это два разных соединения. На внешнем участке обычно нужен HTTPS; внутри отдельной локальной сети приложение может слушать HTTP. Адрес, который видит пользователь, не обязан совпадать с внутренним адресом процесса. Например, запрос к /api/health может обслужить сервис node:3000.
Обратный прокси отличается от перенаправления. При redir браузер получает новый URL и сам идёт по нему. При reverse_proxy браузер продолжает работать с прежним адресом, а внутренний запрос выполняет Caddy. Это полезно для API, потому что внутреннее устройство не требуется раскрывать каждому клиенту. Но приложение всё равно должно корректно формировать ссылки, cookies и абсолютные адреса с учётом своего внешнего URL.
В мастерской схема показывает именно этот путь: входной адрес, совпавшее правило, URI после обработки и целевой сервис. Сравнивайте части по отдельности. Верный домен ещё не доказывает верный маршрут; ответ 200 ещё не означает, что его вернул нужный backend. Для проверки пригодятся тело ответа, диагностический заголовок и журнал сервера.
Официальная документация: Reverse proxy, Заголовки запроса.
Мини-мастерская: соберите Caddyfile и проверьте смысл маршрута
Выберите один из четырёх вариантов: общий портал, статический сайт, PHP или Node.js. Затем переключите локальный HTTP и публичный HTTPS. Введите домен для примера, настройте upstream и путь API, сравните сохранение префикса с его удалением. Конструктор обновит Caddyfile, команды и пояснение маршрута. Файл можно скопировать или скачать, а полный проект — взять из архива.
Начните с общего портала и запроса к API. Посмотрите, какой путь получает Node.js. Затем включите удаление префикса и сравните результат. Это не сетевой тест: мастерская объясняет, что должна сделать сгенерированная конфигурация. Фактическую доступность upstream и ответ приложения проверяют после запуска Caddy. Под формой находятся практические рецепты с объяснением применения.
Открыть мастерскую на весь экран ↗Скачать Caddy, PHP и Node.js: исходники ZIP ↓
caddy validate, HTTP-проверка и проверка сертификата подтверждают разные части результата. Не вводите в публичную мастерскую пароли и ключи DNS-провайдера: для её работы они не нужны.Windows PowerShell: скачиваем и запускаем полный стенд
В Windows установите Docker Desktop по официальной инструкции и включите Linux-контейнеры. После запуска Docker Desktop команда docker version должна показывать и Client, и Server. PHP, Node.js и Caddy отдельно устанавливать не требуется. Выберите свободный каталог: пример ниже создаёт caddy-lab внутри C:\Projects. Если устройство Compose пока незнакомо, поможет предыдущая практическая статья.
Первый запуск, проверка API и настоящая запись Node.js
New-Item -ItemType Directory -Force C:\Projects | Out-Null
Set-Location C:\Projects
Invoke-WebRequest https://ae-nekrasov.ru/wp-content/plugins/aen-caddy-workshop/caddy-project.zip -OutFile caddy-project.zip
Expand-Archive caddy-project.zip -DestinationPath caddy-lab
Set-Location .\caddy-lab\caddy-project
if (-not (Test-Path .env)) { Copy-Item .env.example .env }
docker compose config --quiet
docker compose up -d --build --wait
docker compose ps
Invoke-RestMethod http://127.0.0.1:8088/api/health
$body = @{service='support'; quantity=1} | ConvertTo-Json
Invoke-RestMethod http://127.0.0.1:8088/api/actions -Method Post -ContentType 'application/json' -Body $body
Invoke-RestMethod http://127.0.0.1:8088/api/actions
Откройте http://127.0.0.1:8088. Ожидается три healthy-сервиса и JSON со status: ok. С главной страницы можно перейти к PHP-заявкам и Node.js-панели, сохранить заявку через форму и запустить поток событий. Внешний HTTP-порт привязан к loopback; PHP-FPM и Node.js отдельно на компьютере не публикуются. Если 8088 занят, измените APP_PORT в .env и используйте новый порт в адресах. Существующий .env команды сохраняют.
Сначала пройдите стенд без изменений: отправьте учебную заявку, вызовите JSON API и запустите обновления. Только после этого меняйте один маршрут или заголовок. Такой порядок позволяет отличить ошибку установки от ошибки нового правила. Для обычного завершения используется docker compose down; удаление томов относится к отдельному сбросу данных, а не к каждой остановке.
Официальная документация: Сеть Docker Compose, Именованные тома Docker, Docker Desktop для Windows.
Linux Bash: тот же локальный результат с нуля
Для локальной Linux-машины нужны Docker Engine и Compose plugin; подробная установка на Ubuntu 24.04 для VPS приведена ниже. Здесь предполагается, что Docker уже отвечает. Если ваш пользователь не имеет административного доступа к Docker, запускайте docker-команды через sudo. Команды загрузки и распаковки выполняются в новом пользовательском каталоге.
Linux: загрузка, запуск, запись и чтение результата
mkdir -p ~/projects/caddy-lab
cd ~/projects/caddy-lab
curl -fL https://ae-nekrasov.ru/wp-content/plugins/aen-caddy-workshop/caddy-project.zip -o caddy-project.zip
unzip -n caddy-project.zip
cd caddy-project
test -f .env || cp .env.example .env
docker compose config --quiet
docker compose up -d --build --wait
docker compose ps
curl --fail http://127.0.0.1:8088/api/health
curl --fail http://127.0.0.1:8088/api/actions -H 'Content-Type: application/json' -d '{"service":"support","quantity":1}'
curl --fail http://127.0.0.1:8088/api/actions
Этот старт сохраняет локальную привязку 127.0.0.1. Если выполняете его на удалённом VPS для предварительной проверки, откройте SSH-туннель с рабочего компьютера: ssh -L 8088:127.0.0.1:8088 user@your-server. Затем используйте тот же адрес браузера на своём компьютере. Для реального размещения на домене предусмотрен отдельный серверный файл: он добавляет HTTPS и закрывает доступ к учебным операциям паролем.
Версия Caddy и воспроизводимость примеров
На дату подготовки, 7 ноября 2026 года, актуальный релиз в официальном GitHub API — Caddy 2.11.9. В нём исправлена, в частности, обработка некоторых PHP directory index после предыдущего выпуска. Это хороший пример того, почему номер версии и release notes важны даже для короткого конфига: маршруты могут зависеть от поведения конкретного обработчика.
В проекте используются закреплённые версии образов, а не плавающий тег latest. Они описывают проверенный набор. Если вы обновляете Caddy, PHP или Node.js, повторите проверку маршрутов и форм. Тег образа удобен для воспроизведения, но для строгой фиксации содержимого используется digest. Не требуется менять версии только потому, что конструктор сгенерировал другой домен.
Официальная документация: Изменения Caddy 2.11.9.
Адрес сайта: почему :80, localhost и домен ведут себя по-разному
Первая строка блока задаёт адрес сайта и влияет на выбор протокола. Запись :80 удобна для локального HTTP-стенда: она не выражает намерение получить публичный сертификат для домена. Запись demo.example.com в обычной конфигурации предполагает HTTPS и автоматизацию сертификата. Явный http://demo.example.com описывает HTTP-сайт.
Минимальный локальный HTTP-сервер
:80 {
respond "Локальный HTTP-вход"
}
Имена localhost и внутренние адреса относятся к другой ситуации доверия: сертификат локального центра сертификации не становится автоматически доверенным на всех компьютерах. Кроме того, внешний порт Docker и порт внутри контейнера — разные значения. Если хост публикует 8088 на контейнерный 80, правило внутри Caddy по-прежнему слушает 80. Это важно при чтении конфигурации и редиректов.
Официальная документация: Устройство Caddyfile, Глобальные параметры и локальное доверие.
Автоматический HTTPS: что Caddy делает за вас
Для подготовленного публичного домена Caddy может получить сертификат, обслуживать TLS и затем продлевать сертификат без ручного копирования файлов при каждом обновлении. Он также организует переход с HTTP на HTTPS в типичной конфигурации. Для владельца небольшого проекта это уменьшает число отдельных операций, которые нужно помнить и выполнять в правильный момент.
Схема публичного HTTPS: домен и внутренний Node.js backend
app.example.com {
reverse_proxy node:3000
}
Этот пример описывает намерение, а не готовую публикацию любого имени. Замените адрес своим доменом и обеспечьте правильное DNS-направление к серверу. Внешний трафик должен попадать в Caddy; выбранная проверка центра сертификации должна быть доступна. Успешная генерация Caddyfile в браузере никак не подтверждает DNS, открытые порты или право владения доменом.
Официальная документация: Подготовка HTTPS-сайта, Автоматический HTTPS.
DNS, проверки владения, продление и хранение сертификатов
При обычной публикации проверьте A-запись, а при наличии IPv6 — и AAAA-запись. Устаревший IPv6-адрес способен создавать непонятные частичные отказы. Для стандартных HTTP- и TLS-проверок используются доступные извне порты 80 и 443. Наличие HTTPS между вашим браузером и сервером не заменяет проверку доступности со стороны центра сертификации.
DNS challenge подходит для других условий, в том числе wildcard-сертификатов, но требует соответствующей интеграции с DNS-провайдером и её настройки. Модуль провайдера может отсутствовать в стандартной сборке Caddy. Это отдельный вариант развёртывания, а не переключатель, который заработает от одного флажка в конструкторе. Для экспериментов с выдачей сертификатов полезна тестовая ACME-среда, чтобы не расходовать производственные лимиты.
Caddy хранит состояние сертификатов и учётных записей. В контейнерной схеме это хранилище должно переживать пересоздание контейнера. Не стоит начинать каждое обновление с пустого тома: серверу придётся заново получать данные, а у внешнего центра действуют ограничения. Наблюдайте ошибки продления заранее, пока текущий сертификат ещё действителен, и включайте хранилище в продуманный план восстановления.
Официальная документация: TLS и центры сертификации, Хранилище и соглашения Caddy, Постоянная работа сервера.
Локальный HTTPS: сертификат, доверие и проверка без обхода ошибок
Для локального опыта в архиве предусмотрена отдельная TLS-конфигурация. Она использует tls internal: Caddy создаёт локальный центр сертификации и сертификат для localhost. Такой стенд помогает проверить настоящее TLS-соединение, но не подтверждает выдачу публичного сертификата. Браузеры других устройств не обязаны доверять созданному корневому сертификату.
Проверка выполняется с явным указанием публичного корневого сертификата. Скрипт Python из архива создаёт SSL-контекст с этим CA и проверяет имя localhost; curl со сборкой на OpenSSL позволяет сделать то же через --cacert. Системное хранилище доверия не меняется. Закрытые ключи остаются в служебном томе; в исходники и публикацию их помещать не требуется.
Не смешивайте три действия: создать сертификат, доверять его издателю и подключиться по правильному имени. Сертификат для localhost не подтверждает произвольный домен. Порт 9443 в локальном примере относится к опубликованному порту Docker. Сначала запустите основной стенд, затем добавьте TLS-сервис и экспортируйте только публичный корневой сертификат.
Локальный TLS в Windows PowerShell: для проверочного скрипта нужен Python 3
docker compose -f compose.yaml -f compose.tls.yaml up -d --wait
New-Item -ItemType Directory -Force .runtime | Out-Null
docker compose -f compose.yaml -f compose.tls.yaml cp tls:/data/caddy/pki/authorities/local/root.crt .runtime/caddy-root.crt
python scripts/check_https.py .runtime/caddy-root.crt https://localhost:9443/api/echo
Linux: полный локальный TLS-опыт с проверкой сертификата
docker compose -f compose.yaml -f compose.tls.yaml up -d --wait
mkdir -p .runtime
docker compose -f compose.yaml -f compose.tls.yaml cp tls:/data/caddy/pki/authorities/local/root.crt .runtime/caddy-root.crt
curl --cacert .runtime/caddy-root.crt https://localhost:9443/api/echo
# Если нужен тот же переносимый Python/OpenSSL-клиент:
python3 scripts/check_https.py .runtime/caddy-root.crt https://localhost:9443/api/echo
У системного Windows curl с Schannel может возникнуть ошибка проверки отзыва: локальный CA Caddy не предоставляет привычную для такого клиента инфраструктуру CRL. В проверенном запуске поэтому использован Python/OpenSSL, сохранивший обязательную проверку сертификата и имени. Ошибка одного TLS-клиента не является поводом добавлять -k. В Linux или Git Bash с curl на OpenSSL можно выполнить curl --cacert .runtime/caddy-root.crt https://localhost:9443/api/echo; посмотреть TLS-библиотеку помогает curl -V.
Если сертификат ещё создаётся и команда копирования не нашла файл, посмотрите журнал TLS-сервиса и повторите копирование после его готовности. Скрипт должен вывести tlsVerified: true, hostnameVerified: true и JSON от Node.js с forwardedProto: https. Это подтверждает локальную TLS-цепочку и проход запроса к приложению. Файл .runtime/caddy-root.crt относится только к вашему экземпляру стенда.
Конфигурация локального TLS из проекта
Отдельный сервис для TLS-опыта
{
skip_install_trust
auto_https disable_redirects
}
https://localhost {
tls internal
encode zstd gzip
header X-Content-Type-Options nosniff
reverse_proxy node:3000
log {
output stdout
format json
}
}
Официальная документация: TLS и центры сертификации, Глобальные параметры и локальное доверие.
Пример 1. Статический сайт, файлы и загрузки
Для лендинга, документации, собранного фронтенда или каталога файлов не всегда нужен серверный язык. Caddy может отдавать содержимое выбранной папки напрямую. Это также полезно внутри большого приложения: картинки, CSS и готовый ZIP не должны проходить через PHP или Node.js, если никакой проверки доступа для них не требуется.
Статические файлы из /srv
:80 {
root * /srv
encode zstd gzip
file_server
}
Директива root задаёт расположение файлов, а file_server включает их обслуживание. Одного root недостаточно. Для запроса к /assets/app.css проверяйте файл внутри смонтированного каталога, а не только наличие похожего пути на компьютере. В контейнере абсолютный путь относится к файловой системе контейнера. Опция просмотра каталога нужна лишь тогда, когда вы действительно хотите показывать список файлов.
Статический сайт хорошо подходит для отдельного интерфейса, который обращается к API на другом маршруте того же домена. Файлы конфигурации и данные приложения храните за пределами публичного root. Здесь это простое следствие назначения папки: всё опубликованное через файловый сервер рассматривается как доступный ресурс.
Официальная документация: Отдача статических файлов, Корень сайта.
Пример 2. PHP-FPM: формы и серверные страницы
Caddy сам не исполняет PHP. Он передаёт запрос FastCGI-процессу, например PHP-FPM, а тот выполняет скрипт и возвращает ответ. Для этого предназначена директива php_fastcgi. Обычный reverse_proxy по HTTP не является заменой: PHP-FPM на порту 9000 ожидает другой протокол. Если перепутать их, открытый порт не превратит соединение в рабочее приложение.
Самостоятельный PHP-сайт в сети Compose
:80 {
root * /srv/php/public
php_fastcgi php:9000
file_server
}
В архиве PHP-приложение принимает учебную заявку через форму и показывает сохранённые записи. Это даёт видимый результат работы всей цепочки: Caddy принял запрос, PHP-FPM выполнил код, данные сохранились, браузер получил страницу. Валидация формы и защита её действия относятся к PHP-приложению; настройка прокси не выполняет их за него.
Имя php здесь является именем сервиса Compose. На обычном сервере вместо него может использоваться TCP-адрес или Unix socket. Выбирайте вариант, который соответствует реально запущенному PHP-FPM и его правам доступа. Версия PHP и набор расширений также остаются частью окружения приложения.
Официальная документация: PHP через FastCGI, PHP-FPM.
Корень PHP-приложения, index.php и совпадение путей
В типичном PHP-приложении многие URL обслуживает один front controller — index.php. Адрес вроде /orders/42 не обязан соответствовать физическому файлу. php_fastcgi включает привычную для такого приложения схему поиска и перенаправления к index.php. Если старое приложение устроено иначе, правила нужно адаптировать осознанно, а не добавлять ещё один случайный rewrite.
Caddy и PHP-FPM должны согласованно понимать путь к скрипту. В учебном стенде код доступен им по одинаковому пути /srv/php/public. Это упрощает чтение конфигурации. Если внутри контейнеров пути различаются, можно настроить соответствующий root для FastCGI, но нужно проверить фактический SCRIPT_FILENAME. Частая причина ошибки — файл существует у Caddy, а PHP ищет его в другом каталоге.
Публикуйте именно публичную папку приложения. Файл .env, служебные каталоги, журналы и данные не должны оказаться рядом с общедоступным index.php только ради удобного монтирования. В реальном фреймворке обычно уже есть предложенная структура public; лучше следовать ей и проверить существующий статический файл, обычный маршрут приложения и несуществующий адрес отдельно.
Официальная документация: PHP через FastCGI, Корень сайта, Поиск файла и fallback.
Пример 3. Node.js за reverse proxy
Node.js-приложение обычно само говорит по HTTP. Caddy принимает внешний запрос и передаёт его внутреннему HTTP-серверу. Так удобно публиковать API, сервис отчётов или отдельный компонент административной панели. Внешнему клиенту нужен адрес сайта, а внутренний номер порта становится частью инфраструктуры.
Все запросы получает Node.js на порту 3000
:80 {
reverse_proxy node:3000
}
В Docker имя node находится через внутреннюю сеть Compose. Адрес 127.0.0.1:3000 внутри контейнера Caddy указывал бы на сам контейнер Caddy. Поэтому пример с localhost из руководства для одного сервера нельзя механически переносить в несколько контейнеров. Сам Node.js-сервис должен слушать интерфейс, доступный другим контейнерам, а не только свой loopback.
В скачанном стенде Node.js отдаёт интерфейс и JSON API, а поток событий показывает обновления через прокси. Можно увидеть тело ответа и путь, который получил backend. Для самостоятельного API полезны подходы из руководства по Fastify; Caddy решает задачу входа и маршрутизации, а фреймворк — контракт самого API.
Создать учебную операцию через Caddy и прочитать список с сервера
$payload = @{ service = 'report'; quantity = 2 } | ConvertTo-Json
Invoke-RestMethod -Method Post `
-Uri http://127.0.0.1:8088/api/actions `
-ContentType 'application/json' -Body $payload
Invoke-RestMethod http://127.0.0.1:8088/api/actions
Первый запрос получает HTTP 201 и созданную запись, второй — текущий список. Перезапуск браузера не должен обнулять сохранённые операции: данные находятся у приложения. Это полезная проверка всего пути, потому что один красивый интерфейс ещё не подтверждает работу HTTP-прокси и серверного хранения.
Официальная документация: Первый обратный прокси, Сеть Docker Compose, HTTP в Node.js.
Пример 4. handle и handle_path: сохранять или удалять префикс
Пусть браузер обращается к /api/orders. Если приложение само описывает маршрут /api/orders, префикс нужно сохранить. Если backend ожидает /orders, внешний префикс можно удалить на прокси. Именно это различие важно при выборе handle и handle_path. Оба варианта полезны; правильный определяется контрактом приложения.
Фрагменты внутри блока сайта: сохранение и удаление пути
# Backend получит /api/orders
handle /api/* {
reverse_proxy node:3000
}
# Другой маршрут: backend получит /echo
handle_path /strip/* {
reverse_proxy node:3000
}
В нашем портале /api/* сохраняется, а /strip/echo превращается в /echo. В мастерской можно менять правило и видеть ожидаемый URI. Проверяйте отдельно адрес с завершающим слешем и без него: matcher /api/* не является обещанием совпадения с любым словом, начинающимся на api. Для точного корня обычно задают отдельное правило или понятный редирект.
Сравнить поле url в двух настоящих ответах Node.js
curl "http://127.0.0.1:8088/api/echo?source=keep"
curl "http://127.0.0.1:8088/strip/echo?source=strip"
Удаление префикса запроса не переписывает автоматически HTML, абсолютные ссылки, cookies или redirects, которые формирует приложение. Если оно рассчитано на корень домена, публикация в подпапке может потребовать настройки base URL. Когда такой настройки нет, отдельный поддомен часто оказывается проще. Это стоит решить до изменения десятка rewrite-правил.
Официальная документация: Группы обработчиков, Удаление префикса пути, Правила сопоставления запросов.
Пример 5. SPA и API на одном домене
У SPA маршруты интерфейса часто обслуживает браузерный роутер. Переход внутри страницы работает, а обновление /dashboard/reports приводит к 404, потому что сервер ищет физический файл. Для интерфейсных маршрутов используют fallback к index.html. Но если применить его без разделения ко всем запросам, несуществующий API может получить HTML вместо JSON.
Сначала отдельная группа API, затем fallback интерфейса
:80 {
handle /api/* {
reverse_proxy node:3000
}
handle {
root * /srv
try_files {path} /index.html
file_server
}
}
Разделение через handle позволяет описать взаимоисключающие группы. API остаётся ответственностью backend, а статическая ветка обслуживает интерфейс. После изменения проверьте вложенную страницу, существующий ресурс, неизвестный API-маршрут и ответ с ошибкой. Если у JSON-клиента появляется сообщение про неожиданный символ «<», это часто признак HTML-страницы там, где ожидались данные.
Официальная документация: Типовые схемы Caddyfile, Поиск файла и fallback, Группы обработчиков.
Пример 6. Живые обновления: SSE и WebSocket
Личный кабинет может показывать ход импорта, новые события или изменение статуса без перезагрузки. Для однонаправленного потока от сервера подходит SSE, для двустороннего обмена — WebSocket. Caddy reverse_proxy поддерживает такие соединения; часто не требуется вручную копировать набор Upgrade-заголовков из конфигурации другого сервера.
В Node.js-примере архива поток событий конечный: он выдаёт несколько учебных обновлений и завершается. Это позволяет наблюдать доставку через Caddy без бесконечного фонового подключения. Проверьте, что обновления приходят постепенно, а не одной пачкой в конце. Приложение должно корректно объявлять тип потока и управлять его завершением.
Для рабочего сервиса важны переподключение клиента и поведение при обновлении прокси. Долгоживущее соединение не является гарантией непрерывной доставки. После reconnect интерфейсу нужен способ восстановить актуальное состояние. Например, для фоновых задач можно сначала запросить текущий статус, а затем подписаться на события; сами очереди разобраны в мастерской BullMQ.
Официальная документация: Reverse proxy, HTTP в Node.js.
HTTPS до приложения: два соединения и два доверия
Замок в браузере описывает соединение браузера с Caddy. Между Caddy и backend может быть другое соединение. Если приложение находится на удалённом сервере или требования предусматривают шифрование внутреннего участка, upstream задают через HTTPS. Проверка сертификата upstream должна соответствовать его имени и доверенному издателю.
HTTPS снаружи и HTTPS до удалённого backend
app.example.com {
reverse_proxy https://backend.example.net
}
Сертификат сервера выбирается с учётом имени, а HTTP Host может влиять на маршрутизацию приложения. В актуальных версиях Caddy часть поведения для HTTPS upstream настроена автоматически; старые советы с обязательным ручным Host нужно сверять с используемой версией. Если обращаетесь к IP, а сертификат выдан на имя, настройте корректное имя TLS и доверие, вместо отключения проверки.
В учебной сети Compose upstream использует HTTP, а PHP — FastCGI. Это отдельные внутренние сервисы без опубликованных портов. При переносе стенда на сервер заново оцените границы сети и способ доступа. Наличие Docker-сети само по себе не описывает всю модель доверия большой инфраструктуры.
Официальная документация: Reverse proxy, TLS и центры сертификации.
Пример 7. Сжатие и размер ответа
Сжатие полезно для текстовых ресурсов: HTML, CSS, JavaScript и достаточно больших JSON-ответов. Директива encode позволяет Caddy выбрать поддерживаемое клиентом кодирование. В наших примерах используются zstd и gzip. Проверять эффект нужно на реальном теле ответа: короткое сообщение или уже сжатое изображение не обязаны стать заметно меньше.
Директива внутри блока сайта
encode zstd gzip
Клиент сообщает допустимые форматы через Accept-Encoding. В ответе можно проверить Content-Encoding и сравнить размер передачи. Если инструмент автоматически распаковывает тело, число символов в прочитанном тексте не равно числу переданных байтов. Для диагностики сначала убедитесь, что ответ действительно пришёл от проверяемого маршрута, затем смотрите заголовки и размер.
Сжатие не заменяет оптимизацию запроса к базе или уменьшение лишних данных. API, который собирает большой отчёт тридцать секунд, не станет мгновенным только из-за gzip. Здесь помогает разделение ответственности: Caddy оптимизирует передачу, приложение — вычисление, а очередь — долгую работу, которую можно выполнить отдельно.
Официальная документация: Сжатие ответов.
Заголовки ответа и сведения о запросе
Заголовки помогают клиенту понять ответ и позволяют разработчику проверить маршрут. В учебном портале можно увидеть маркер обслуживающего слоя. Для собственных экспериментов удобно временно добавить понятный диагностический заголовок, вызвать разные URL и убедиться, что правило применяется там, где ожидалось.
Пример заголовков ответа внутри блока сайта
header {
X-Content-Type-Options nosniff
X-Demo-Gateway "Caddy workshop"
}
Не переносите большой набор заголовков без понимания назначения. Политика кеширования зависит от содержимого: личный JSON-ответ и версионированный CSS требуют разных правил. CSP зависит от скриптов, стилей и ресурсов страницы. HSTS имеет смысл после подготовки HTTPS и выбранной политики доменов, а не как случайная строка в локальном HTTP-примере.
Для информации о клиентском адресе и исходном протоколе прокси использует соответствующие заголовки. Если перед Caddy есть ещё один прокси или CDN, настраивайте доверие только к ожидаемым источникам. Приложение должно знать, каким сведениям оно доверяет. Значение из произвольного клиентского заголовка не становится достоверным только потому, что его название похоже на служебное.
Официальная документация: Заголовки ответа, Заголовки запроса, Reverse proxy.
Пример 8. Журнал запросов: понять, что произошло
Access log отвечает на вопрос, какой запрос пришёл и какой ответ получил клиент. Runtime log помогает понять работу самого Caddy: запуск, ошибки конфигурации, соединений и сертификатов. Это разные уровни наблюдения. При сбое полезно связать время запроса, URI, статус и сообщение приложения, а не смотреть только последнюю красную строку.
HTTP-журнал в stdout для контейнерного стенда
log {
output stdout
format json
}
В Docker такой журнал удобно читать через docker compose logs --tail=100 caddy. Для рабочего сервиса продумайте срок хранения и сбор логов. Логировать всё бесконечно — не то же самое, что иметь полезную диагностику. Токены в URL и чувствительные параметры создают проблемы уже на уровне дизайна API; не превращайте диагностический журнал в хранилище секретов.
Разбор полезно начинать с воспроизводимого запроса. Запишите ожидаемый маршрут и результат, повторите запрос один раз, найдите его в журнале и сравните со статусом backend. Если на странице показывается 404, сначала выясните, кто его вернул: file_server, PHP-приложение или Node.js. От ответа зависит дальнейшее исправление.
Официальная документация: Журнал HTTP-запросов, Устройство журналов.
404, 502 и обработка ошибок: сохраняем смысл ответа
404 означает, что запрошенный ресурс или маршрут не найден, но источник ответа ещё нужно установить. 502 часто указывает на проблему связи прокси с upstream: неверное имя, порт, протокол или остановленное приложение. Полезная страница ошибки объясняет, что произошло, и сохраняет соответствующий HTTP-статус. Красивый экран с кодом 200 может мешать мониторингу и клиентам API.
Обработка ошибок, которые возникли в маршруте Caddy
handle_errors {
respond "Учебный сервер: ошибка {err.status_code}" {err.status_code}
}
handle_errors не перехватывает автоматически каждый ответ 4xx или 5xx от приложения. Если upstream успешно ответил HTTP 404, это может быть нормальный ответ backend, а не ошибка обработчика Caddy. Для целенаправленного преобразования ответов прокси используется другой механизм, например handle_response. Не подменяйте JSON-ошибку API HTML-страницей без согласованного контракта.
В учебном стенде можно сравнить неизвестный адрес и временно недоступный Node.js. После восстановления сервиса запрос снова должен проходить. Такая проверка полезнее, чем лишь убедиться, что главная страница открылась один раз. Она показывает, куда смотреть при конкретном отказе и как выглядит восстановление.
Официальная документация: Обработка ошибок Caddy, Явная ошибка для маршрута.
Форматирование, проверка и reload без случайной остановки
Перед применением конфигурации полезно проверить её тем же Caddy, который будет её обслуживать. fmt приводит текст к принятому оформлению. adapt показывает преобразование Caddyfile во внутренний JSON. validate идёт дальше синтаксиса и проверяет подготовку конфигурации, но не доказывает доступность каждого приложения и успешную выдачу публичного сертификата.
Проверить и применить файл внутри учебного контейнера
docker compose exec caddy caddy validate --config /etc/caddy/Caddyfile --adapter caddyfile
docker compose exec caddy caddy adapt --config /etc/caddy/Caddyfile --adapter caddyfile --pretty
docker compose exec caddy caddy reload --config /etc/caddy/Caddyfile --adapter caddyfile
Reload предназначен для применения конфигурации к работающему процессу через административный интерфейс. Этот интерфейс не требуется публиковать на внешнем адресе. Сохраните предыдущую рабочую конфигурацию, проверьте новую и затем повторите несколько характерных HTTP-запросов. Если поменялся образ или состав сервисов Compose, одного reload недостаточно: это уже изменение окружения.
Caddyfile не всегда выполняется буквально сверху вниз: адаптер учитывает порядок директив. Для взаимоисключающих веток используйте handle, для специально заданной последовательности обработчиков — route. Если правило кажется правильным, но результат другой, просмотр адаптированного JSON помогает увидеть фактическую структуру, а не угадывать её по отступам.
Официальная документация: Команды Caddy, Порядок директив, Явный порядок обработчиков, Административный API.
Полный портал: Caddy, PHP и Node.js в одной сети
В общем стенде снаружи опубликован только вход Caddy. Статические файлы находятся в его root, PHP-FPM получает код по согласованному пути, Node.js отвечает на внутреннем порту. Такая схема позволяет проверить несколько технологий вместе и сохраняет понятную границу: браузер взаимодействует с одним адресом, а маршруты направляют запрос дальше.
Папки с исходниками и папки с данными имеют разное назначение. Код можно пересобрать из архива, а сохранённые заявки и состояние TLS требуют отдельного хранения. Именованные тома переживают обычное пересоздание контейнеров. Это не заменяет резервную копию, но позволяет не терять данные при каждом штатном обновлении.
Полный Caddyfile портала из архива
Рабочая конфигурация общего входа
# Локальный HTTP: Compose публикует 127.0.0.1:8088 -> контейнер:80.
:80 {
encode zstd gzip
header X-Content-Type-Options nosniff
header Referrer-Policy strict-origin-when-cross-origin
log {
output stdout
format json
}
# Сохраняем префикс /api перед передачей Node.js.
handle /api/* {
reverse_proxy node:3000
}
# Отдельный маршрут для сравнения: /strip/echo -> /echo.
handle_path /strip/* {
reverse_proxy node:3000
}
@nodeUI path /node /node/* /events
handle @nodeUI {
reverse_proxy node:3000
}
redir /php /php/ 308
handle_path /php/* {
root * /srv/php/public
php_fastcgi php:9000
file_server
}
handle {
root * /srv
file_server
}
}
Полный compose.yaml из архива
Контейнеры, сети, порты и тома проекта
name: aen-caddy
services:
caddy:
image: caddy:2.11.9-alpine
ports:
- "127.0.0.1:${APP_PORT:-8088}:80"
volumes:
- ./config:/etc/caddy:ro
- ./public:/srv:ro
- ./php/public:/srv/php/public:ro
- caddy_data:/data
- caddy_config:/config
depends_on:
php:
condition: service_healthy
node:
condition: service_healthy
healthcheck:
test: ["CMD", "wget", "-q", "-O", "/dev/null", "http://127.0.0.1/health"]
interval: 5s
timeout: 3s
retries: 12
restart: unless-stopped
php:
build:
context: .
dockerfile: php/Dockerfile
volumes:
- ./php/public:/srv/php/public:ro
- php_data:/var/demo-data
healthcheck:
test: ["CMD", "php", "-r", "$$s=@fsockopen('127.0.0.1',9000);exit($$s?0:1);"]
interval: 5s
timeout: 3s
retries: 12
restart: unless-stopped
node:
build:
context: .
dockerfile: node/Dockerfile
volumes:
- node_data:/app/data
environment:
PORT: 3000
healthcheck:
test: ["CMD", "node", "-e", "fetch('http://127.0.0.1:3000/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"]
interval: 5s
timeout: 3s
retries: 12
init: true
stop_grace_period: 10s
restart: unless-stopped
volumes:
caddy_data:
caddy_config:
php_data:
node_data:
После изменения bind-mounted конфигурации примените её предусмотренной командой reload. После изменения кода, копируемого в образ, пересоберите соответствующий сервис. Не путайте restart и build: перезапуск прежнего контейнера не создаёт новый образ из изменённых файлов. В README описаны команды для состава именно этого проекта.
Официальная документация: Сеть Docker Compose, Именованные тома Docker.
Linux VPS: от чистой Ubuntu до работающего Docker
Локальная практика полезна, пока вы разбираете маршруты. Следующий результат — собственный закрытый веб-инструмент на облачном или выделенном сервере: единый домен, HTTPS, форма PHP, Node.js API и сохранение записей между перезапусками. В архиве для этого есть самостоятельный deploy/compose.server.yaml. Он не объединяется с локальными compose.yaml и compose.tls.yaml: так внешние порты и постоянные тома остаются очевидными.
Дальнейшие команды рассчитаны на свежую Ubuntu 24.04 LTS с SSH и sudo. Дистрибутив поддерживается официальным репозиторием Docker. Если сервер уже обслуживает проекты, сначала проверьте docker version, docker compose version и занятые порты командой sudo ss -lntp. Не заменяйте установленный Docker и не останавливайте существующий прокси ради примера. На отдельном новом VPS можно пройти весь путь ниже.
Официальный репозиторий Docker для новой Ubuntu 24.04
cat /etc/os-release
sudo apt-get update
sudo apt-get install -y ca-certificates curl unzip python3
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
docker_arch=$(dpkg --print-architecture)
sudo tee /etc/apt/sources.list.d/docker.sources >/dev/null <<EOF
Types: deb
URIs: https://download.docker.com/linux/ubuntu
Suites: noble
Components: stable
Architectures: $docker_arch
Signed-By: /etc/apt/keyrings/docker.asc
EOF
sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
sudo systemctl enable --now docker
sudo docker version
sudo docker compose version
Эти действия собраны также в scripts/install-docker-ubuntu.sh с проверкой ОС и отказом при уже установленном Docker. Docker daemon запускается через systemd; контейнеры проекта имеют restart: unless-stopped. Поэтому после перезагрузки хоста Docker поднимет сервисы, которые вы не остановили вручную. Отдельный systemd-unit вокруг docker compose up здесь не требуется. Для доступа к Docker в серверных командах используется sudo: членство в группе docker также даёт широкие административные права.
У домена app.your-domain.tld должна быть A-запись с публичным IPv4 сервера. AAAA добавляйте только при реально работающем IPv6. На сетевом экране хостинга разрешите входящий TCP 80 и 443; SSH оставьте доступным по своей административной политике. Порты 3000, 9000, 2019 и 8081 открывать не нужно. В этом примере HTTP/3 через UDP не публикуется; обычный HTTPS работает по TCP. Учтите правила Docker для firewall: опубликованные контейнерные порты могут обходить обычные правила UFW, поэтому проверьте и привязки, и внешний сетевой экран.
Официальная документация: Установка Docker на Ubuntu, Автоматический запуск контейнеров, Публикация портов и firewall.
Файлы релиза, домен и закрытый вход
Разместим код в /opt/aen-caddy/releases/v1/caddy-project. Ссылка current будет указывать на текущий релиз, а секретный env-файл останется отдельно в /etc/aen-caddy. Эта структура помогает вернуться к предыдущему коду: проект содержит bind mounts статических файлов и PHP, поэтому при откате нужна прежняя папка релиза, а не только прежний тег Docker-образа.
Первое размещение: каталог v1 и ссылка current ещё не существуют
sudo install -d -m 0755 /opt/aen-caddy/releases/v1
sudo curl -fL https://ae-nekrasov.ru/wp-content/plugins/aen-caddy-workshop/caddy-project.zip -o /opt/aen-caddy/releases/v1/source.zip
sudo unzip /opt/aen-caddy/releases/v1/source.zip -d /opt/aen-caddy/releases/v1
sudo ln -s /opt/aen-caddy/releases/v1/caddy-project /opt/aen-caddy/current
cd /opt/aen-caddy/current
sha256sum /opt/aen-caddy/releases/v1/source.zip
Можно передать свой подготовленный архив через scp из Windows PowerShell или Linux Bash: scp ./caddy-project.zip user@your-server:~/caddy-project.zip. На сервере распакуйте его в тот же каталог releases/v1 вместо повторного скачивания. Сохраняйте исходный ZIP, его SHA-256 и использованные версии. Код и конфигурация релиза принадлежат root и читаются контейнерами; записываемые пользовательские данные находятся в named volumes, а не в публичном каталоге.
Шаблон копируется один раз, до заполнения настроек
cd /opt/aen-caddy/current
sudo install -d -m 0700 /etc/aen-caddy
sudo install -m 0600 deploy/site.env.example /etc/aen-caddy/site.env
sudo docker run --rm -it caddy:2.11.9-alpine caddy hash-password
sudoedit /etc/aen-caddy/site.env
Команда hash-password интерактивно попросит новый пароль. Запишите в SITE_DOMAIN ваш реальный домен без https:// и пути. BASIC_AUTH_USER оставьте reader или задайте своё простое имя. В BASIC_AUTH_HASH вставьте полученный bcrypt-хеш в одинарных кавычках: они сохраняют символы $ при чтении env-файла Compose. Пароль не нужно передавать в аргументе команды и оставлять в истории shell. Остальные значения шаблона подходят для первого развёртывания.
Шаблон параметров: реальный хеш заполняется только на вашем сервере
# Copy to /etc/aen-caddy/site.env; mode0600. Never publish the filled file.
DEPLOY_PROJECT=aen-caddy-site
SITE_DOMAIN=app.example.com
BASIC_AUTH_USER=reader
# Paste bcrypt output inside SINGLE quotes, preserving all dollar signs.
BASIC_AUTH_HASH=''
CADDY_IMAGE=caddy:2.11.9-alpine
IMAGE_PREFIX=aen-caddy-demo
APP_RELEASE=v1
EDGE_BIND=0.0.0.0
HTTP_PORT=80
HTTPS_PORT=443
CADDY_CONFIG_DIR=.
Не копируйте шаблон повторно поверх заполненного site.env. Не кладите его в public, Git или ZIP. Файл имеет права 0600, каталог — 0700. Пароль доступа сообщается только участникам закрытого стенда. Этот общий вход подходит для приватной демонстрации или небольшого внутреннего инструмента. Для клиентского продукта нужны личные аккаунты, роли, аудит и собственная модель доступа. Учебные JSON-хранилища и лимит 100 записей остаются учебными: Caddy не превращает их в полноценную бизнес-систему.
Официальная документация: Закрытый вход basic_auth, Переменные и кавычки Compose.
Запуск на домене: HTTPS, PHP и Node.js
Серверная конфигурация публикует только Caddy на 80/443. PHP-FPM и Node.js доступны внутри сети Docker и не имеют host-портов. Каждый запрос к приложению сначала проходит basic_auth; затем Caddy убирает Authorization и выбирает маршрут. Секреты не передаются дальше в учебное приложение. Порядок закреплён блоком route, а данные, сертификаты и конфигурационное хранилище Caddy размещены в четырёх постоянных томах.
Полный серверный Caddyfile из исходников
{
admin localhost:2019
}
{$SITE_DOMAIN} {
encode zstd gzip
header {
X-Content-Type-Options nosniff
Referrer-Policy same-origin
}
log {
output stdout
format json
}
route {
basic_auth {
{$BASIC_AUTH_USER} {$BASIC_AUTH_HASH}
}
# Authenticate at the edge, then do not forward its password to apps.
request_header -Authorization
redir /php /php/ 308
handle /api/* {
reverse_proxy node:3000
}
handle_path /strip/* {
reverse_proxy node:3000
}
@nodeUI path /node /node/* /events
handle @nodeUI {
reverse_proxy node:3000
}
handle_path /php/* {
root * /srv/php/public
php_fastcgi php:9000
file_server
}
handle {
root * /srv
file_server
}
}
}
# Internal health endpoint: this port is not published by Compose.
http://:8081 {
respond /health "ok"
}
Каталог deploy монтируется целиком в /etc/caddy только для чтения. Это позволяет редактировать Caddyfile обычным редактором с атомарным сохранением и затем делать reload: контейнер видит новую версию файла. Веб-корень при этом находится отдельно, в /srv; env, Dockerfile и серверные исходники Node.js не становятся скачиваемыми файлами сайта. Admin API слушает localhost внутри контейнера и не публикуется.
Сначала проверка, затем запуск самостоятельного серверного файла
cd /opt/aen-caddy/current
sudo docker compose --env-file /etc/aen-caddy/site.env -f deploy/compose.server.yaml config --quiet
sudo docker compose --env-file /etc/aen-caddy/site.env -f deploy/compose.server.yaml build php node
sudo docker compose --env-file /etc/aen-caddy/site.env -f deploy/compose.server.yaml run --rm --no-deps caddy caddy validate --config /etc/caddy/Caddyfile --adapter caddyfile
sudo docker compose --env-file /etc/aen-caddy/site.env -f deploy/compose.server.yaml up -d --no-build --wait
sudo docker compose --env-file /etc/aen-caddy/site.env -f deploy/compose.server.yaml ps
Ожидается три healthy-сервиса. Проверка Caddy обращается к внутреннему HTTP-порту 8081; она подтверждает работу процесса, но не выдачу публичного сертификата. Для публичного HTTPS Caddy должен подтвердить домен, сохранить сертификат в /data и иметь возможность продлевать его. Не удаляйте том caddy_data при обновлении. Входящий HTTP автоматически перенаправляется на HTTPS; basic auth применяется уже к защищённому соединению.
Подставьте свой домен; –user reader спросит пароль интерактивно
curl -I http://app.your-domain.tld/
curl -I https://app.your-domain.tld/
curl --fail --user reader https://app.your-domain.tld/api/health
curl --fail --user reader 'https://app.your-domain.tld/php/?format=json'
curl --fail --user reader -N https://app.your-domain.tld/events
Первый запрос должен показать перенаправление на https://. Второй без пароля — 401, что подтверждает закрытый вход. С паролем Node.js возвращает status: ok, PHP — JSON со счётчиком заявок, а /events выдаёт пять progress-событий и done. В браузере пройдите вход, сохраните вымышленную заявку и операцию Node.js. Обновите страницы и убедитесь, что записи остались. Так вы проверяете полезный результат всей цепочки, а не только наличие запущенного контейнера.
Если домен не открывается, сначала проверьте DNS и внешний firewall, затем журналы caddy. Если сайт отвечает 502, проверьте healthy-состояние PHP/Node и правильность внутреннего адреса. Не отключайте TLS-проверку, чтобы скрыть ошибку сертификата. Здесь описан воспроизводимый путь на вашем VPS; внешний сервер, публичный ACME и реальный DNS при подготовке материала не изменялись.
Официальная документация: Автоматический HTTPS, Хранилище и соглашения Caddy, Compose на рабочем сервере.
Эксплуатация: журналы, reload и перезапуск
В ежедневной работе чаще нужен журнал конкретного сервиса и аккуратное изменение маршрута. Access log Caddy пишет JSON в stdout; стандартная настройка скрывает чувствительные заголовки, включая Authorization. Docker ограничивает журнал каждого контейнера тремя файлами по 10 MB. Это защита от бесконтрольного роста, а не полноценное централизованное хранение: нужные события и сроки хранения определяются вашим сервисом.
После успешного validate применяем конфигурацию и проверяем ответ
cd /opt/aen-caddy/current
sudo docker compose --env-file /etc/aen-caddy/site.env -f deploy/compose.server.yaml logs --tail 100 caddy php node
sudoedit deploy/Caddyfile
sudo docker compose --env-file /etc/aen-caddy/site.env -f deploy/compose.server.yaml exec -T caddy caddy validate --config /etc/caddy/Caddyfile --adapter caddyfile
sudo docker compose --env-file /etc/aen-caddy/site.env -f deploy/compose.server.yaml exec -T caddy caddy reload --config /etc/caddy/Caddyfile --adapter caddyfile
curl --fail --user reader https://app.your-domain.tld/api/health
Если validate завершился ошибкой, исправьте файл и не выполняйте reload. Для изменения SITE_DOMAIN или BASIC_AUTH_HASH одного reload недостаточно: окружение уже созданного контейнера не меняется от редактирования env-файла. После проверки site.env выполните docker compose с теми же параметрами –env-file и -f, командой up -d –no-build –wait. Compose пересоздаст затронутый сервис с новым окружением. При ротации пароля также обновите его у пользователей.
Штатная остановка и запуск уже созданных контейнеров
sudo docker compose --env-file /etc/aen-caddy/site.env -f deploy/compose.server.yaml stop
sudo docker compose --env-file /etc/aen-caddy/site.env -f deploy/compose.server.yaml start --wait
sudo systemctl status docker --no-pager
restart: unless-stopped не запускает вручную остановленный контейнер автоматически и не лечит каждое состояние unhealthy. Мониторинг должен проверять реальный HTTPS-адрес и ожидаемый ответ приложения. Обычный down сохраняет named volumes; down -v удаляет данные и сертификаты, поэтому для штатного обслуживания он не нужен. Не публикуйте полный docker compose config в общий лог: туда попадают подставленные значения окружения.
Официальная документация: Команды Caddy, Журнал HTTP-запросов, Автоматический запуск контейнеров.
Резервная копия данных и сертификатов
У этого небольшого стенда нет распределённой базы: PHP и Node.js пишут JSON, а Caddy хранит сертификаты и служебное состояние. Поэтому резервирование сделано через короткое окно обслуживания. Скрипт backup-server.py требует работающие три сервиса, останавливает их, сохраняет четыре тома и приватный site.env, записывает SHA-256 и снова запускает те же контейнеры. start –wait возвращает прежние контейнеры, не применяя случайно заранее отредактированный Compose или новый образ.
Linux: backup временно делает сайт недоступным, затем возвращает его в работу
cd /opt/aen-caddy/current
sudo install -d -m 0700 /var/backups/aen-caddy
sudo python3 scripts/backup-server.py --env /etc/aen-caddy/site.env --output /var/backups/aen-caddy
Windows: тот же инструмент для отдельного серверного стенда
# Из корня проекта: env должен описывать запущенный серверный вариант
python scripts/backup-server.py --env C:\Private\site.env --output C:\Backups\aen-caddy
Результат — новый каталог с UTC-временем, caddy_data.tar.gz, caddy_config.tar.gz, php_data.tar.gz, node_data.tar.gz, site.env и manifest.json. В Linux каталог получает 0700, файлы — 0600. На Windows дополнительно используйте закрытый каталог с подходящими NTFS-правами: chmod там не заменяет настройку ACL. Копии содержат приватные ключи TLS и данные приложения, поэтому их нельзя класть на сайт, присылать как исходники или хранить в открытом репозитории.
Скрипт пытается возобновить сервисы и при обычной ошибке копирования. Если пропало питание, Docker остановился или процесс был принудительно завершён, проверьте ps и самостоятельно выполните start –wait. Неполный каталог без корректного manifest не считается готовой копией. Сохраните отдельно соответствующий ZIP релиза: backup томов не включает bind-mounted код. Для защиты от потери диска перенесите копию и исходники релиза в независимое закрытое хранилище; при переносе сохраните доступность файлов только администраторам.
Официальная документация: Именованные тома Docker.
Восстановление в новые тома и контролируемое переключение
Резервная копия становится полезной после проверки восстановления. Скрипт restore-volumes.py сначала сверяет SHA-256, проверяет имена файлов внутри архивов, затем создаёт четыре новых тома. Он отказывается перезаписывать существующий том и не запускает сайт. Это позволяет подготовить данные отдельно от работающего экземпляра. Имя проекта должно начинаться с aen-caddy-restore- и быть новым для каждого опыта.
Замените время в пути на точный каталог, напечатанный backup
sudo python3 scripts/restore-volumes.py --backup /var/backups/aen-caddy/20261107T120000000000Z --project aen-caddy-restore-check1
python scripts/restore-volumes.py --backup C:\Backups\aen-caddy\20261107T120000000000Z --project aen-caddy-restore-check1
Скрипт напечатает новые имена томов и оставит отчёт рядом с backup. Для восстановления используйте код того же релиза, копию сохранённого site.env и новый DEPLOY_PROJECT. Остальные параметры, особенно APP_RELEASE и образ Caddy, должны соответствовать сохранённому варианту. Новый префикс автоматически подключает подготовленные тома. Не запускайте два экземпляра на одних внешних портах и не подключайте два записывающих приложения к одному JSON-хранилищу.
Проверка подготовленных данных на домене после освобождения 80/443
# На том же сервере, из каталога соответствующего релиза:
sudo install -m 0600 /var/backups/aen-caddy/20261107T120000000000Z/site.env /etc/aen-caddy/restore.env
sudoedit /etc/aen-caddy/restore.env
# Измените DEPLOY_PROJECT=aen-caddy-restore-check1; остальные параметры сохраните.
sudo docker compose --env-file /etc/aen-caddy/restore.env -f deploy/compose.server.yaml config --quiet
# Начало согласованного окна обслуживания, запись в прежний сервис прекращается:
sudo docker compose --env-file /etc/aen-caddy/site.env -f deploy/compose.server.yaml stop
sudo docker compose --env-file /etc/aen-caddy/restore.env -f deploy/compose.server.yaml up -d --no-build --wait
curl --fail --user reader 'https://app.your-domain.tld/php/?format=json'
curl --fail --user reader https://app.your-domain.tld/api/actions
Сравните число и содержимое записей с ожидаемыми. Восстановление возвращает состояние на время backup: более новые записи отсутствуют. Если восстановленный вариант принят, сохраните прежний site.env под отдельным именем и сделайте проверенный restore.env активной конфигурацией для дальнейшего обслуживания. Оставьте прежние тома до завершения приёмки. При восстановлении на новом VPS сначала установите Docker, загрузите соответствующий код и образы, настройте DNS и только затем запускайте восстановленный проект.
Возврат к прежним контейнерам и томам до замены активного env-файла
# Откат пробного переключения на том же сервере:
sudo docker compose --env-file /etc/aen-caddy/restore.env -f deploy/compose.server.yaml stop
sudo docker compose --env-file /etc/aen-caddy/site.env -f deploy/compose.server.yaml start --wait
curl --fail --user reader https://app.your-domain.tld/api/health
Сертификаты также восстановлены из caddy_data. Это сохраняет состояние Caddy, но не отменяет его обычную проверку сроков и последующее продление. Для теста восстановления без публичного домена используйте отдельный локальный CA и доверенный файл только у проверяющего клиента; не отключайте проверку TLS и не выдавайте такую проверку за выпуск публичного сертификата.
Обновление релиза и граница проверенного результата
Для нового кода создайте releases/v2/caddy-project, сохраните прежний env и сделайте backup. Перенесите свои настройки deploy/Caddyfile в новый релиз осознанно, сравнив изменения. В рабочем site.env измените APP_RELEASE на v2: так новые PHP/Node-образы получат отдельные теги. Имя DEPLOY_PROJECT оставьте прежним, чтобы подключить прежние данные. Код PHP и статика берутся из папки релиза; Node.js и базовая среда PHP — из собранных образов.
Обновление с сохранением прежнего каталога и образов
sudo cp -p /etc/aen-caddy/site.env /etc/aen-caddy/site.env.before-v2
sudoedit /etc/aen-caddy/site.env
cd /opt/aen-caddy/releases/v2/caddy-project
sudo docker compose --env-file /etc/aen-caddy/site.env -f deploy/compose.server.yaml config --quiet
sudo docker compose --env-file /etc/aen-caddy/site.env -f deploy/compose.server.yaml build php node
sudo docker compose --env-file /etc/aen-caddy/site.env -f deploy/compose.server.yaml run --rm --no-deps caddy caddy validate --config /etc/caddy/Caddyfile --adapter caddyfile
sudo docker compose --env-file /etc/aen-caddy/site.env -f deploy/compose.server.yaml up -d --no-build --wait
curl --fail --user reader https://app.your-domain.tld/api/health
# После проверки PHP-формы, Node.js и сохранности данных:
sudo ln -sfn /opt/aen-caddy/releases/v2/caddy-project /opt/aen-caddy/current
Для планового обновления самого Caddy задайте проверенный тег в CADDY_IMAGE, выполните pull caddy и обычный up с теми же –env-file и -f. Перед заменой изучите изменения версии и проверьте конфигурацию. Полный тег закрепляет версию; для строгого соответствия образа используйте digest. Не выполняйте глобальный prune в ходе такого обновления: прежние образы и тома нужны для возврата.
Откат использует и старые образы, и старую папку bind mounts
# Откат к прежнему коду и сохранённым настройкам:
sudo cp -p /etc/aen-caddy/site.env.before-v2 /etc/aen-caddy/site.env
cd /opt/aen-caddy/releases/v1/caddy-project
sudo docker compose --env-file /etc/aen-caddy/site.env -f deploy/compose.server.yaml up -d --no-build --wait
curl --fail --user reader https://app.your-domain.tld/api/health
sudo ln -sfn /opt/aen-caddy/releases/v1/caddy-project /opt/aen-caddy/current
Смена образа не откатывает структуру данных. Если будущая версия приложения изменит формат JSON или перейдёт на БД, потребуется совместимый план миграции и восстановления. Текущий пример показывает один сервер и короткое окно обслуживания, а не развёртывание без простоя или отказоустойчивый кластер.
Проверка дополнения выполнена в отдельном проекте Linux-контейнеров Docker Desktop: HTTPS с проверкой имени и локального CA, отказ 401 без пароля, PHP CSRF и Secure-cookie, запись через PHP и Node.js, пять SSE-событий, reload, backup четырёх томов, запуск восстановленных данных в новых томах и отказ повторно перезаписать их. Исходный локальный HTTP-стенд продолжал отвечать. Отчёт evidence/platform.json содержит отдельные результаты. Установка Ubuntu, публичный ACME, настоящий VPS и доверие браузера на физическом устройстве здесь не объявляются выполненной проверкой.
Официальная документация: Compose на рабочем сервере, Постоянная работа сервера, Автоматический HTTPS.
Шпаргалка диагностики: искать причину по участку цепочки
| Симптом | Первая проверка | Практический вывод |
|---|---|---|
| Домен ведёт не на тот сервер | A/AAAA и фактический адрес ответа | Исправлять маршрут DNS, а не PHP-код |
| Сертификат не выпускается | Доступность challenge и журнал Caddy | Сгенерированный конфиг не подтверждает домен |
| Локальному сертификату не доверяют | Имя в сертификате и доверенный CA | tls internal не создаёт публичное доверие |
| 502 при обращении к API | Имя сервиса, порт, протокол и состояние Node.js | localhost внутри контейнера может указывать не туда |
| PHP отвечает ошибкой пути | Совпадение root и файла в обоих контейнерах | Проверить реальный путь к index.php |
| JSON-клиент получил HTML | Разделение API и SPA fallback | Не отправлять API в index.html |
| Подпапка открылась, ссылки сломались | Base URL, redirects и абсолютные пути приложения | strip_prefix меняет запрос, но не весь ответ |
| Новая конфигурация не действует | Какой файл загружен и был ли reload | Правка файла и применение — разные шаги |
Базовые команды наблюдения
docker compose ps
docker compose logs --tail=100 caddy
docker compose logs --tail=100 php
docker compose logs --tail=100 node
curl -i http://127.0.0.1:8088/api/health
В Windows PowerShell для вызова именно curl используйте curl.exe, если имя curl в вашей версии оболочки занято другим инструментом. Сохраняйте смысл проверки: статус, заголовок и тело ответа важнее конкретной команды. Если после исправления меняется только одна часть цепочки, повторите запрос, который выявил проблему, и соседний рабочий маршрут.
Исходники, проверка результата и дальнейшие шаги
В архиве находятся конфигурации Caddy, Docker Compose, PHP-приложение, Node.js-приложение, браузерная мастерская, README, лицензии и проверки. Начните с общего портала. Затем выберите самостоятельный PHP- или Node.js-вариант и сравните, как сокращается конфигурация. После этого проще перенести один понятный рецепт в собственный проект.
Проверка должна подтверждать результат, который видит человек: заявка действительно сохранена, API возвращает ожидаемый JSON, поток приходит постепенно, префикс пути меняется согласно правилу. Для HTTPS дополнительно проверяются сертификат и доверие. Проверенный локальный TLS не объявляется проверенной выдачей публичного сертификата: последняя зависит от вашего домена, сети и выбранного центра сертификации.
evidence/config-validation.json, evidence/final-build.json, evidence/http-smoke.json, evidence/lifecycle.json, evidence/platform.json, evidence/tls.json. Они фиксируют проверенный запуск и границы каждого опыта. После изменения конфигурации или версий повторите соответствующие сценарии в своей среде.Открыть мастерскую на весь экран ↗Скачать Caddy, PHP и Node.js: исходники ZIP ↓
Если задача сайта — просто отдать готовые файлы, начните со статического варианта. Если нужно соединить интерфейс и API, разберите handle и путь до backend. Если публикуете PHP, уделите внимание FastCGI и файловым путям. Такой порядок помогает использовать Caddy как понятный инструмент для конкретной задачи, а не накапливать конфигурацию, назначение которой уже никто не помнит.
Официальная документация: Документация Caddy, Типовые схемы Caddyfile.


