certbot init without certificates was fixed.

This commit is contained in:
febux 2026-08-21 18:47:00 +03:00
parent 674c52b6c6
commit c70ac6e310
3 changed files with 85 additions and 28 deletions

View file

@ -2,14 +2,27 @@
set -euo pipefail set -euo pipefail
# certbot-init.sh — obtain the first Let's Encrypt certificate for the # certbot-init.sh — obtain the first Let's Encrypt certificate for the
# «Контракт-чек» edge proxy and reload nginx. # «Контракт-чек» edge proxy and (re)start nginx.
# #
# Run this once on the VPS after DNS points the domain at this server and the # Bootstrap (first run, no certificate yet):
# certbot container is started (see DEPLOY.md). # nginx CANNOT start before a certificate exists — the rendered config
# references /etc/letsencrypt/live/<domain>/*.pem and nginx exits if those
# files are missing. So the first certificate is obtained in --standalone
# mode: certbot binds port 80 itself (nginx is down at this point, so the
# port is free). After issuance the script starts nginx; the certbot renew
# sidecar then uses the webroot plugin against the running nginx.
#
# Subsequent runs (certificate already exists): webroot renew via the running
# nginx + `nginx -s reload`.
# #
# Usage: # Usage:
# chmod +x deploy/nginx/certbot-init.sh # chmod +x deploy/nginx/certbot-init.sh
# ./deploy/nginx/certbot-init.sh your-domain.example.com admin@example.com # ./deploy/nginx/certbot-init.sh your-domain.example.com admin@example.com
#
# Requirements:
# - DNS A-record for the domain points at this server
# - NGINX_SERVER_NAME=<same domain> is set in .env
# - the `services` profile is up (api healthy) — nginx depends on it
DOMAIN="${1:-}" DOMAIN="${1:-}"
EMAIL="${2:-}" EMAIL="${2:-}"
@ -19,21 +32,54 @@ if [[ -z "$DOMAIN" || -z "$EMAIL" ]]; then
exit 1 exit 1
fi fi
# The certbot container in docker-compose mounts these directories. if [[ -f .env ]] && ! grep -qE "^NGINX_SERVER_NAME=${DOMAIN}\s*$" .env; then
# It must be running so certbot can write the webroot challenge response. echo "ERROR: NGINX_SERVER_NAME in .env must equal ${DOMAIN}." >&2
echo " nginx renders server_name and the certificate path from it." >&2
echo " Add/fix: NGINX_SERVER_NAME=${DOMAIN}" >&2
exit 1
fi
# nginx depends on api (service_healthy), and api is gated behind the
# `services` profile — both profiles must be enabled for compose to
# resolve the dependency graph.
DC=(docker compose --profile services --profile edge)
CERTBOT_WEBROOT=/var/www/certbot CERTBOT_WEBROOT=/var/www/certbot
LE_DIR=/etc/letsencrypt LE_DIR=/etc/letsencrypt
docker compose --profile edge run --rm --entrypoint "certbot certonly \ cert_exists() {
--webroot \ "${DC[@]}" run --rm --entrypoint sh certbot \
--webroot-path ${CERTBOT_WEBROOT} \ -c "test -f ${LE_DIR}/live/${DOMAIN}/fullchain.pem" >/dev/null 2>&1
--agree-tos \ }
--no-eff-email \
--email ${EMAIL} \
-d ${DOMAIN} \
--non-interactive" certbot
# Reload nginx so it picks up the new certificate. if cert_exists; then
docker compose --profile edge exec nginx nginx -s reload echo ">>> Certificate already exists — renewing via webroot (nginx serves the challenge)."
"${DC[@]}" run --rm --entrypoint "certbot certonly \
--webroot \
--webroot-path ${CERTBOT_WEBROOT} \
--agree-tos \
--no-eff-email \
--email ${EMAIL} \
-d ${DOMAIN} \
--non-interactive \
--keep-until-expiring" certbot
else
echo ">>> No certificate yet — obtaining via standalone on port 80."
# nginx cannot boot without a certificate (see header), so port 80 is free.
"${DC[@]}" stop nginx >/dev/null 2>&1 || true
"${DC[@]}" run --rm -p 80:80 --entrypoint "certbot certonly \
--standalone \
--agree-tos \
--no-eff-email \
--email ${EMAIL} \
-d ${DOMAIN} \
--non-interactive" certbot
fi
echo "Certificate issued for ${DOMAIN}. Nginx reloaded." # Start (or re-create) nginx + the certbot renew sidecar.
"${DC[@]}" up -d nginx certbot
# Reload in case nginx was already running with an old certificate.
"${DC[@]}" exec nginx nginx -s reload || true
echo "Certificate issued for ${DOMAIN}. Nginx (re)started."

View file

@ -432,9 +432,6 @@ services:
image: nginx:alpine image: nginx:alpine
container_name: contract_check-nginx container_name: contract_check-nginx
restart: unless-stopped restart: unless-stopped
depends_on:
api:
condition: service_healthy
ports: ports:
- "80:80" - "80:80"
- "443:443" - "443:443"
@ -448,7 +445,7 @@ services:
NGINX_ENVSUBST_OUTPUT_DIR: /etc/nginx/conf.d NGINX_ENVSUBST_OUTPUT_DIR: /etc/nginx/conf.d
NGINX_ENVSUBST_TEMPLATE_SUFFIX: .template NGINX_ENVSUBST_TEMPLATE_SUFFIX: .template
healthcheck: healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost/healthz"] test: ["CMD", "wget", "-qO-", "--no-check-certificate", "http://localhost/healthz"]
interval: 10s interval: 10s
timeout: 3s timeout: 3s
retries: 10 retries: 10

View file

@ -509,29 +509,43 @@ docker compose --profile services logs -f bot
NGINX_SERVER_NAME=contract-check.example.com NGINX_SERVER_NAME=contract-check.example.com
``` ```
> **Важно:** nginx зависит от `api` (`service_healthy`), а `api` находится в
> профиле `services` — поэтому nginx/certbot всегда запускайте с обоими
> профилями: `docker compose --profile services --profile edge ...`.
> Запуск только `--profile edge` падает с `service "nginx" depends on
> undefined service "api"`.
```bash ```bash
# Запуск edge вместе со стеком # Запуск edge вместе со стеком (после получения первого сертификата, см. §13.2)
NGINX_SERVER_NAME=contract-check.example.com \ docker compose --profile services --profile edge up -d
docker compose --profile services --profile edge up -d
``` ```
### 13.2 Первый запуск и получение сертификата ### 13.2 Первый запуск и получение сертификата
DNS A-запись должна указывать на IP сервера **до** запуска certbot. DNS A-запись должна указывать на IP сервера **до** запуска certbot.
`NGINX_SERVER_NAME` в `.env` должен совпадать с доменом.
Nginx **не может стартовать без сертификата** — рендеренный конфиг ссылается на
`/etc/letsencrypt/live/<domain>/fullchain.pem`, и nginx падает на старте, если
файла нет. Поэтому первый сертификат получается в `--standalone` режиме
(certbot сам слушает порт 80, nginx в этот момент не запущен), и только затем
nginx стартует. Всё это делает `certbot-init.sh`:
```bash ```bash
# 1. Запускаем edge без HTTPS (HTTP-челлендж для certbot работает на порту 80) # 1. Запускаем основной стек (nginx пока НЕ стартуем)
NGINX_SERVER_NAME=contract-check.example.com \ docker compose --profile services up -d
docker compose --profile edge up -d nginx certbot
# 2. Получаем первый сертификат # 2. Получаем первый сертификат (standalone, порт 80) и стартуем nginx + renew-sidecar
chmod +x deploy/nginx/certbot-init.sh chmod +x deploy/nginx/certbot-init.sh
./deploy/nginx/certbot-init.sh contract-check.example.com admin@example.com ./deploy/nginx/certbot-init.sh contract-check.example.com admin@example.com
# 3. Проверяем # 3. Проверяем
https://contract-check.example.com/healthz curl https://contract-check.example.com/healthz
``` ```
При повторном запуске скрипт делает renew через webroot (nginx уже отдаёт
челленджи) и `nginx -s reload`.
По умолчанию `nginx.conf` проксирует: По умолчанию `nginx.conf` проксирует:
- `/api/v1/*`, `/admin/*` - `/api/v1/*`, `/admin/*`
- `/healthz`, `/readyz` - `/healthz`, `/readyz`