#!/usr/bin/env bash set -euo pipefail # certbot-init.sh — obtain the first Let's Encrypt certificate for the # «Контракт-чек» edge proxy and (re)start nginx. # # Bootstrap (first run, no certificate yet): # nginx CANNOT start before a certificate exists — the rendered config # references /etc/letsencrypt/live//*.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: # chmod +x deploy/nginx/certbot-init.sh # ./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= is set in .env # - the `services` profile is up (api healthy) — nginx depends on it DOMAIN="${1:-}" EMAIL="${2:-}" if [[ -z "$DOMAIN" || -z "$EMAIL" ]]; then echo "Usage: $0 " exit 1 fi # Resolve NGINX_SERVER_NAME the same way compose does: shell env wins over .env. # Tolerates quotes, inline comments ("domain # comment"), CRLF and padding — # a hostname never contains whitespace, so the first token is the value. configured="${NGINX_SERVER_NAME:-}" if [[ -z "$configured" && -f .env ]]; then raw="$(sed -nE 's/^NGINX_SERVER_NAME=//p' .env | tail -n1 | tr -d "\"'\r")" read -r configured _ <<<"$raw" || true fi if [[ "$configured" != "$DOMAIN" ]]; then echo "ERROR: NGINX_SERVER_NAME must equal ${DOMAIN} (found: '${configured:-}')." >&2 echo " nginx renders server_name and the certificate path from it." >&2 echo " Fix .env in the project root: NGINX_SERVER_NAME=${DOMAIN}" >&2 echo " (or export NGINX_SERVER_NAME=${DOMAIN} before running this script)" >&2 echo " Run the script from the project root — the dir containing .env." >&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 LE_DIR=/etc/letsencrypt cert_exists() { "${DC[@]}" run --rm --entrypoint sh certbot \ -c "test -f ${LE_DIR}/live/${DOMAIN}/fullchain.pem" >/dev/null 2>&1 } if cert_exists; then 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 # 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."