Этот репозиторий — готовый шаблон для развертывания production-ready reverse-proxy на Nginx. Он включает надежный процесс получения и автоматического обновления SSL-сертификатов от Let's Encrypt и CI/CD пайплайн для безопасного деплоя через self-hosted runner GitHub Actions.
У вас есть один сервер, и вы хотите:
- Запускать на нем несколько сайтов или сервисов.
- Обеспечить безопасное HTTPS-соединение для всех доменов без ручной настройки.
- Автоматизировать выкат изменений конфигурации Nginx простым
git push. - Не беспокоиться о лимитах Let's Encrypt и случайных сбоях при обновлении сертификатов.
Этот шаблон предоставляет централизованное, стабильное и автоматизированное решение для этих задач.
- Динамический Reverse Proxy: Конфигурация Nginx генерируется "на лету" из шаблона, используя переменные окружения.
- Надежное управление SSL: Однократное получение сертификата и автоматическое обновление через cron, что исключает проблемы с лимитами Let's Encrypt.
- CI/CD "из коробки": Готовый пайплайн на GitHub Actions, который при пуше в
masterобновляет только Nginx. - Безопасность: Никаких секретов и приватных данных в коде. Все управляется через GitHub Secrets.
- Разделяемая архитектура: Легко подключайте любые внешние приложения к прокси через общую Docker-сеть.
Система разделяет прокси-сервисы и ваши приложения. Они взаимодействуют через внешнюю Docker-сеть net.
- Прокси-сервис (этот репозиторий):
nginx: При старте генерирует свой конфиг изnginx.conf.template. Обрабатывает трафик, терминирует SSL и проксирует запросы.certbot: Не запускается автоматически. Используется как инструмент для ручного получения и автоматического обновления сертификатов через cron.
- Бэкенд-сервис (
your-app): Ваше приложение, подключенное к сетиnet.
- Сервер с публичным IP.
- Доменное имя, A-запись которого указывает на IP сервера.
- Установленные Docker и Docker Compose.
- Настроенный self-hosted runner GitHub Actions на сервере с доступом к Docker.
В вашем репозитории на GitHub перейдите в Settings > Secrets and variables > Actions и создайте три секрета:
DOMAIN_1: Основной домен (например,domain1.example.pw).DOMAIN_2: Второй домен (например,domain2.example.pw).EMAIL: Ваш email для уведомлений от Let's Encrypt.
Эту команду нужно выполнить на сервере один раз:
sudo docker network create netСделайте первый коммит и пуш в master. GitHub Actions запустит деплой. Этот первый деплой, скорее всего, завершится неудачей, так как у Nginx еще нет SSL-сертификатов. Это нормально и ожидаемо. Нам нужен запущенный Nginx, чтобы Certbot мог подтвердить владение доменом.
Теперь, когда Nginx запущен (пусть и с ошибками), выполните на сервере один раз вручную следующую команду, чтобы получить сертификаты.
Замените ВАШ_EMAIL и ВАШ_ДОМЕН вашими реальными данными в команде ниже:
sudo docker compose run --rm certbot certonly --webroot --webroot-path=/var/www/certbot --email ВАШ_EMAIL -d ВАШ_ДОМЕН --agree-tos --no-eff-emailПосле успешного выполнения этой команды, перезапустите пайплайн в GitHub Actions или сделайте еще один коммит. Теперь Nginx найдет сертификаты и запустится корректно.
Добавьте задачу в cron на вашем сервере, чтобы сертификаты обновлялись автоматически. Команды cron лучше запускать от имени root, так как docker требует sudo.
Откройте crontab для root:
sudo crontab -eИ добавьте следующую строку, заменив /path/to/your/project/ на реальный путь к вашему проекту на сервере (например, /opt/actions-runner/apps/relaxy):
30 4 * * * cd /path/to/your/project/ && docker compose run --rm certbot renew && docker compose exec nginx nginx -s reload >> /var/log/cron-cert-renew.log 2>&1Эта задача будет выполняться каждый день в 4:30 утра. Она делает три вещи:
- Пытается обновить сертификат (
certbot renew). - После попытки обновления всегда выполняет команду
nginx -s reload, чтобы применить изменения, если они были. - Записывает результат в лог-файл для отладки.
Примечание об автоматической перезагрузке Nginx: Хотя Certbot предлагает флаг
--deploy-hookдля выполнения команд только после успешного обновления, его использование в этом Docker-окружении затруднено. Команда хука выполняется внутри контейнераcertbot, у которого нет доступа кdocker composeдля перезагрузкиnginx.Подход с
&&проще и надежнее в данном контексте. Командаnginx -s reloadочень "легкая", и ее безопасный запуск (даже когда сертификат не обновлялся) не создает проблем и является прагматичным решением.
Иногда нужно проверить статус сертификатов или запустить обновление вручную. Все команды выполняются на сервере в директории проекта.
# Запускаем обновление
sudo docker compose run --rm certbot renew
# Применяем новый сертификат в Nginx
sudo docker compose exec nginx nginx -s reload1. Изнутри контейнера (проверка файла на диске)
Замените <ваш_домен> на ваш домен:
sudo docker compose exec nginx openssl x509 -in /etc/letsencrypt/live/ВАШ_ДОМЕН/fullchain.pem -noout -dates2. Снаружи (проверка того, что отдает сервер)
Эту команду можно выполнить с любого компьютера. Замените <ваш_домен> на ваш домен:
openssl s_client -connect ВАШ_ДОМЕН:443 -servername ВАШ_ДОМЕН 2>/dev/null | openssl x509 -noout -dates
#### Проверка логов Certbot
После добавления тома для логов Certbot в `docker-compose.yml`, вы можете просматривать детальные логи Certbot на хост-системе.
1. **Перейдите в директорию логов Certbot на хосте:**
```bash
cd /path/to/your/project/data/certbot/logs
```
(Замените `/path/to/your/project/` на реальный путь к вашему проекту).
2. **Просмотрите файл лога:**
```bash
tail -f letsencrypt.log
```
или
```bash
less letsencrypt.log
```
### Шаг 6: Обычный режим работы
Все готово! Теперь при каждом `git push` в `master` GitHub Actions будет автоматически и безопасно обновлять только конфигурацию Nginx.
## ➕ Как добавить новый сервис для проксирования
1. **Настройте ваше приложение**: В `docker-compose.yml` вашего приложения подключите его к сети `net`.
2. **Добавьте location в Nginx**: Откройте `nginx.conf.template` и добавьте новый `location`.
3. **Сделайте `git push`**. GitHub Actions автоматически применит новую конфигурацию Nginx.
## 🌐 Второй домен (`DOMAIN_2`)
Второй домен раздаётся через тот же фронтовый nginx. relaxy остаётся чистым
прокси: всю логику держит **отдельный апстрим-сервис** (свой контейнер,
подключённый к сети `net`), а фронтовый nginx только терминирует TLS и
проксирует. Сейчас апстримом второго домена работает `mp-max-directus:8055`;
раньше на его месте был статический `site-2` с basic auth — смена апстрима
сводится к одной строке `set $upstream_site_2` в `nginx.conf.template`.
Апстрим-приложение может требовать websocket (Directus держит на нём
realtime-подписки) и загрузку крупных файлов, поэтому в блоке второго домена
заданы `proxy_http_version 1.1` с заголовками `Upgrade`/`Connection` и
`client_max_body_size 64m`. Заголовок `Connection` вычисляется через `map` в
начале шаблона: `upgrade` только когда клиент действительно просит апгрейд,
иначе обычные keep-alive запросы ломаются.
- Server-блоки для `$DOMAIN_2` заданы в `nginx.conf.template`; envsubst
подставляет и `$DOMAIN_1`, и `$DOMAIN_2`.
- Домен задаётся секретом `DOMAIN_2` (см. Шаг 1).
- **Сертификат выпускается автоматически в CI** — шаг `Ensure second-domain TLS
certificate` в `deploy.yml` идемпотентен (`--keep-until-expiring`), поэтому
повторные пуши не задевают лимиты Let's Encrypt.
- Обновление сертификата покрыто общим cron: `certbot renew` обновляет все домены.
DNS: A-запись второго домена должна указывать на IP сервера.
> **Важно про первый запуск (bootstrap).**
> Авто-выпуск в CI работает только тогда, когда nginx **уже запущен** — он
> отдаёт ACME-челлендж для `DOMAIN_2` через default `:80`-сервер. В нашем случае
> так и есть: nginx уже обслуживает `DOMAIN_1`, поэтому для добавления второго
> домена ручных действий не требуется.
>
> Но при **чистой установке с нуля сразу с двумя доменами** возникает «курица-яйцо»:
> nginx не стартует, пока нет сертификатов для **обоих** доменов (каждый `:443`-блок
> ссылается на свои cert-файлы), а раз nginx не запущен — CI не сможет пройти
> ACME-челлендж. В этом случае оба сертификата нужно получить вручную **до** первого
> старта nginx. Конфиг ссылается на раздельные каталоги (`live/$DOMAIN_1/` и
> `live/$DOMAIN_2/`), поэтому нужны **два отдельных** вызова (не один с двумя `-d` —
> тот создал бы только каталог первого домена):
> ```bash
> sudo docker compose run --rm certbot certonly --webroot --webroot-path=/var/www/certbot \
> --email ВАШ_EMAIL -d ВАШ_ДОМЕН_1 --agree-tos --no-eff-email
> sudo docker compose run --rm certbot certonly --webroot --webroot-path=/var/www/certbot \
> --email ВАШ_EMAIL -d ВАШ_ДОМЕН_2 --agree-tos --no-eff-email
> ```