#!/bin/bash # init-letsencrypt.sh # Bootstrap SSL certificate for first deployment. # Run ONCE. After this, certbot auto-renews every 12 hours. set -e ENV_FILE=".env.production" COMPOSE_FILE="docker-compose.prod.yml" COMPOSE="docker compose --env-file $ENV_FILE -f $COMPOSE_FILE" # --------------------------------------------------------------------------- # Read config from .env.production # --------------------------------------------------------------------------- DOMAIN=$(grep '^NGINX_DOMAIN=' "$ENV_FILE" | cut -d= -f2 | tr -d ' ') EMAIL=$(grep '^CERTBOT_EMAIL=' "$ENV_FILE" | cut -d= -f2 | tr -d ' ') if [ -z "$DOMAIN" ] || [ -z "$EMAIL" ]; then echo "ERROR: NGINX_DOMAIN and CERTBOT_EMAIL must be set in $ENV_FILE" exit 1 fi echo "==> Domain : $DOMAIN" echo "==> Email : $EMAIL" echo "" # --------------------------------------------------------------------------- # Step 0: Skip bootstrap if a real cert already exists # --------------------------------------------------------------------------- echo "==> Checking for existing certificates..." if $COMPOSE run --rm --entrypoint sh certbot \ -c "test -f /etc/letsencrypt/live/$DOMAIN/fullchain.pem"; then echo "==> Certificates already exist. Starting nginx..." $COMPOSE up -d --wait nginx echo "==> Done. nginx is running with the existing certificate." exit 0 fi echo "==> No certificates found. Starting bootstrap..." echo "" # --------------------------------------------------------------------------- # Step 1: Create a temporary self-signed certificate so nginx can start. # nginx refuses to start if the ssl_certificate path does not exist. # --------------------------------------------------------------------------- echo "==> [1/4] Creating temporary self-signed certificate..." $COMPOSE run --rm --entrypoint sh certbot -c \ "mkdir -p /etc/letsencrypt/live/$DOMAIN && \ openssl req -x509 -nodes -newkey rsa:4096 -days 1 \ -keyout /etc/letsencrypt/live/$DOMAIN/privkey.pem \ -out /etc/letsencrypt/live/$DOMAIN/fullchain.pem \ -subj /CN=localhost \ -quiet" # Verify the cert was actually created before continuing if ! $COMPOSE run --rm --entrypoint sh certbot \ -c "test -f /etc/letsencrypt/live/$DOMAIN/fullchain.pem"; then echo "ERROR: Failed to create temporary certificate. Check the certbot image." exit 1 fi echo "==> Temporary certificate created." # --------------------------------------------------------------------------- # Step 2: Start nginx (and all backend dependencies). # --wait blocks until every health check passes. # --------------------------------------------------------------------------- echo "==> [2/4] Starting nginx and backend services..." $COMPOSE up -d --wait nginx echo "==> nginx is up." # --------------------------------------------------------------------------- # Step 3: Request the real certificate from Let's Encrypt. # The certbot image uses certbot as its entrypoint — pass certonly # and flags directly as the command. # --------------------------------------------------------------------------- echo "==> [3/4] Requesting SSL certificate from Let's Encrypt..." $COMPOSE run --rm certbot certonly \ --webroot \ --webroot-path=/var/www/certbot \ --email "$EMAIL" \ --agree-tos \ --no-eff-email \ -d "$DOMAIN" # --------------------------------------------------------------------------- # Step 4: Reload nginx so it picks up the real certificate. # --------------------------------------------------------------------------- echo "==> [4/4] Reloading nginx with the real certificate..." $COMPOSE exec nginx nginx -s reload echo "" echo "====================================================================" echo " SSL certificate obtained successfully!" echo " Site is live at: https://$DOMAIN" echo "" echo " Next step: run 'make prod-up' to start all remaining services." echo "===================================================================="