Когда в домашней лаборатории появляется несколько веб-сервисов, открывать для каждого новый порт неудобно и небезопасно. Проще оставить наружу только 80 и 443, а запросы распределять по доменам: status.example.ru отправлять в Uptime Kuma, cloud.example.ru — в файловый сервис, vault.example.ru — в менеджер паролей.
Caddy хорошо подходит для этой роли: получает и продлевает TLS-сертификаты, перенаправляет HTTP на HTTPS и умеет проксировать WebSocket без ручного набора заголовков.
Что должно быть готово заранее
Для публичного сертификата проверьте четыре условия:
- A- и, если используется, AAAA-записи домена указывают на ваш сервер;
- TCP-порты
80 и 443 доходят до сервера через роутер и firewall;
- системное время синхронизировано;
- провайдер не держит подключение за CGNAT.
Узнать внешний IPv4 можно через панель провайдера или сервис определения адреса. Сравните его с WAN-адресом роутера. Если адреса разные и у вас нет выделенного IPv6, обычный входящий HTTP-01 challenge работать не будет. В таком случае нужен белый адрес, VPN-туннель с внешним узлом или DNS-01 challenge с модулем вашего DNS-провайдера.
Каталоги и общая Docker-сеть
sudo mkdir -p /opt/caddy/conf
sudo chown -R "$USER":"$USER" /opt/caddy
cd /opt/caddy
docker network create homelab_proxy
Сеть создаётся один раз и не принадлежит конкретному Compose-проекту. Через неё Caddy увидит контейнеры из других стеков по DNS-именам сервисов.
compose.yaml для Caddy
services:
caddy:
image: caddy:2-alpine
restart: unless-stopped
ports:
- "80:80"
- "443:443"
- "443:443/udp"
volumes:
- type: bind
source: ./conf
target: /etc/caddy
read_only: true
- caddy_data:/data
- caddy_config:/config
networks:
- proxy
networks:
proxy:
external: true
name: homelab_proxy
volumes:
caddy_data:
caddy_config:
Каталог conf подключён целиком, а не одним файлом. Официальный образ отдельно предупреждает: некоторые редакторы при сохранении заменяют inode файла, из-за чего bind mount одного Caddyfile может продолжить показывать контейнеру старое содержимое до пересоздания.
/data обязательно сохраняется в volume. Там находятся сертификаты, закрытые ключи, OCSP-данные и другое состояние автоматического HTTPS. Это не кэш, который можно бездумно удалить. /config менее критичен, но его сохранение упрощает обслуживание.
UDP 443 нужен для HTTP/3. Если он не разрешён на роутере, обычные HTTPS по TCP и HTTP/2 продолжат работать; просто не будет HTTP/3.
Первый Caddyfile
Создайте /opt/caddy/conf/Caddyfile:
status.example.ru {
encode zstd gzip
reverse_proxy uptime-kuma:3001
}
dockge.example.ru {
encode zstd gzip
reverse_proxy dockge:5001
}
Замените домены своими. Не добавляйте http:// перед публичным доменом: адрес с нормальным hostname включает automatic HTTPS. Caddy сам получает сертификат и создаёт перенаправление с HTTP.
Запустите стек и смотрите первый выпуск сертификатов в журнале:
docker compose config
docker compose up -d
docker compose ps
docker compose logs -f --tail=100 caddy
На этом этапе upstream-контейнеры ещё могут отсутствовать. Caddy запустится, но запрос к их доменам вернёт 502 до подключения сервисов к общей сети.
Как подключить другой Compose-проект
В стеке Uptime Kuma добавьте внешнюю сеть:
services:
uptime-kuma:
image: louislam/uptime-kuma:2
restart: unless-stopped
volumes:
- uptime_kuma_data:/app/data
networks:
- proxy
networks:
proxy:
external: true
name: homelab_proxy
volumes:
uptime_kuma_data:
Публиковать 3001:3001 больше не требуется: Caddy обращается прямо к uptime-kuma:3001 внутри homelab_proxy. Это уменьшает число входов на хосте.
Не подключайте к общей proxy-сети базу данных без необходимости. Обычно в ней участвуют только Caddy и веб-контейнер приложения. База остаётся во внутренней сети своего стека.
Проверьте участников сети:
docker network inspect homelab_proxy
В списке должны быть Caddy и нужный upstream. Если два стека используют одинаковое имя сервиса, Docker DNS может вернуть не тот контейнер. Давайте публичным сервисам уникальные имена в пределах общей сети.
Проверка и безопасная перезагрузка конфигурации
Перед применением каждого изменения проверьте синтаксис:
docker compose exec caddy \
caddy validate --config /etc/caddy/Caddyfile
Если конфигурация корректна, выполните graceful reload без остановки HTTPS:
docker compose exec caddy \
caddy reload --config /etc/caddy/Caddyfile
Перезапускать контейнер после каждой строки не нужно. Если reload сообщил ошибку, старый рабочий config продолжит обслуживать запросы.
Проверьте результат снаружи домашней сети, например через мобильный интернет:
curl -I https://status.example.ru
Локальная проверка через тот же роутер иногда скрывает ошибку NAT reflection или split DNS.
Почему Caddy возвращает 502
502 Bad Gateway означает, что HTTPS-вход работает, но Caddy не может получить нормальный ответ от upstream.
Проверяйте по порядку:
docker compose logs --tail=100 caddy
docker network inspect homelab_proxy
docker exec -it "$(docker compose ps -q caddy)" wget -S -O- http://uptime-kuma:3001
В Alpine-образе набор утилит может меняться. Если wget отсутствует, используйте временный диагностический контейнер в той же сети:
docker run --rm --network homelab_proxy curlimages/curl:8.16.0 \
-I http://uptime-kuma:3001
Типовые причины:
- upstream не подключён к
homelab_proxy;
- в Caddyfile указано имя контейнера из интерфейса вместо имени Compose-сервиса;
- выбран внешний порт хоста, хотя внутри сети нужен порт контейнера;
- приложение слушает только
127.0.0.1 внутри своего контейнера;
- контейнер постоянно перезапускается из-за собственной ошибки.
Не исправляйте 502 через tls_insecure_skip_verify. Этот параметр отключает проверку сертификата upstream и создаёт ложное ощущение защищённости. В приватной Docker-сети обычно достаточно обычного HTTP до контейнера.
Почему не выпускается сертификат
Смотрите конкретную ACME-ошибку:
docker compose logs --since=15m caddy
Чаще всего обнаруживается одно из следующего:
- DNS ещё указывает на старый IP;
- AAAA-запись существует, но IPv6 до сервера не настроен;
- порт 80 перенаправлен на другой хост;
- firewall разрешает 443, но блокирует 80;
- домен находится за CGNAT;
- было слишком много неудачных попыток и сработал rate limit центра сертификации.
Не удаляйте caddy_data, пытаясь «начать заново»: так вы удалите существующие ключи и можете только увеличить число запросов к ACME. Сначала устраните причину из журнала.
Доступ к административным сервисам
Наличие HTTPS не делает Dockge, Portainer или панель NAS безопасными для всего интернета. Для административных интерфейсов лучше:
- оставить доступ только через WireGuard или другой VPN;
- ограничить исходные IP;
- использовать собственную сильную авторизацию сервиса;
- при необходимости поставить перед приложением отдельный authentication gateway;
- включить второй фактор, если приложение его поддерживает.
Не подключайте /var/run/docker.sock к Caddy. Для reverse proxy ему не нужен доступ к Docker API: имена контейнеров разрешает обычная сеть.
Резервная копия
Сохраните Compose-файл и весь каталог conf в репозитории без секретов. Состояние /data содержит закрытые ключи, поэтому архив нужно шифровать и хранить с ограниченным доступом.
backup_dir="$HOME/backups/caddy-$(date +%Y%m%d-%H%M%S)"
mkdir -p "$backup_dir"
docker compose stop caddy
caddy_container="$(docker compose ps -aq caddy)"
tar -C /opt/caddy -czf "$backup_dir/caddy-config.tar.gz" compose.yaml conf
docker run --rm \
--volumes-from "$caddy_container:ro" \
--mount "type=bind,src=$backup_dir,dst=/backup" \
alpine:3.22 \
tar -C /data -czf /backup/caddy-data.tar.gz .
docker compose start caddy
sha256sum "$backup_dir"/*.tar.gz > "$backup_dir/SHA256SUMS"
Если копирование прервалось, сначала верните Caddy командой docker compose start caddy. Для большинства публичных сертификатов ключи можно перевыпустить, но конфигурацию и защищённую копию всё равно нужно проверять восстановлением.
Обновление
Сначала проверьте release notes выбранной версии и сделайте копию. Затем:
docker compose pull
docker compose up -d
docker compose ps
docker compose logs --since=10m caddy
После обновления откройте несколько разных доменов, проверьте WebSocket-приложение и выполните caddy validate. Не используйте down -v: эта команда удалит volumes с состоянием HTTPS.
Официальные источники