#!/usr/bin/env sh # Copyright (c) Abstract Machines # SPDX-License-Identifier: Apache-2.0 set -eu ROOT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) ENV_FILE="$ROOT_DIR/docker/.env" COMPOSE_FILE="$ROOT_DIR/docker/docker-compose.yaml" HOST=${MG_PUBLIC_HOST:-} EMAIL=${MG_LETSENCRYPT_EMAIL:-} LETSENCRYPT_ENABLED=${MG_LETSENCRYPT_ENABLED:-true} STAGING=${MG_LETSENCRYPT_STAGING:-false} FORCE_RENEWAL=${MG_LETSENCRYPT_FORCE_RENEWAL:-false} PROJECT=${DOCKER_PROJECT:-magistrala} TIMEOUT_SECONDS=${MG_LETSENCRYPT_TIMEOUT_SECONDS:-180} usage() { cat <&2 exit 2 fi case "$LETSENCRYPT_ENABLED" in true|false) ;; *) echo "MG_LETSENCRYPT_ENABLED must be true or false." >&2 exit 2 ;; esac if [ "$LETSENCRYPT_ENABLED" = "true" ] && [ -z "$EMAIL" ]; then usage >&2 exit 2 fi if [ "$LETSENCRYPT_ENABLED" = "true" ] && [ "$HOST" = "localhost" ]; then echo "MG_PUBLIC_HOST must be a public DNS name, not localhost." >&2 exit 2 fi if [ ! -f "$ENV_FILE" ]; then echo "Missing $ENV_FILE" >&2 exit 1 fi set_env() { key=$1 value=$2 tmp=$(mktemp) awk -v key="$key" -v value="$value" ' BEGIN { done = 0 } !done && (index($0, key "=") == 1 || index($0, "#" key "=") == 1) { print key "=" value done = 1 next } { print } END { if (!done) { print key "=" value } } ' "$ENV_FILE" > "$tmp" mv "$tmp" "$ENV_FILE" } comment_env() { key=$1 value=$2 tmp=$(mktemp) awk -v key="$key" -v value="$value" ' BEGIN { done = 0 } !done && (index($0, key "=") == 1 || index($0, "# " key "=") == 1 || index($0, "#" key "=") == 1) { print "# " key "=" value done = 1 next } { print } END { if (!done) { print "# " key "=" value } } ' "$ENV_FILE" > "$tmp" mv "$tmp" "$ENV_FILE" } comment_env_any() { key=$1 tmp=$(mktemp) awk -v key="$key" ' index($0, key "=") == 1 { print "# " $0; next } { print } ' "$ENV_FILE" > "$tmp" mv "$tmp" "$ENV_FILE" } compose() { docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" -p "$PROJECT" "$@" } write_ui_proxy() { ui_host=${MG_UI_HOST:-ui} cat > "$ROOT_DIR/docker/nginx/snippets/ui-proxy.conf" </dev/null 2>&1; then return 0 fi elapsed=0 while [ "$elapsed" -lt "$TIMEOUT_SECONDS" ]; do status=$(curl -s -o /dev/null -w "%{http_code}" --max-time 2 \ http://127.0.0.1/.well-known/acme-challenge/magistrala-tls-probe 2>/dev/null || true) case "$status" in 200|301|302|307|308|404) return 0 ;; esac sleep 2 elapsed=$((elapsed + 2)) done echo "Timed out waiting for Nginx to accept HTTP traffic." >&2 compose logs --tail 80 nginx >&2 || true exit 1 } cert_path="./ssl/letsencrypt/live/$HOST/fullchain.pem" key_path="./ssl/letsencrypt/live/$HOST/privkey.pem" cert_file="$ROOT_DIR/docker/ssl/letsencrypt/live/$HOST/fullchain.pem" key_file="$ROOT_DIR/docker/ssl/letsencrypt/live/$HOST/privkey.pem" if [ "$LETSENCRYPT_ENABLED" = "false" ]; then FORCE_RENEWAL=false fi if [ "$LETSENCRYPT_ENABLED" = "true" ] && [ "$STAGING" = "false" ]; then live_dir="$ROOT_DIR/docker/ssl/letsencrypt/live/$HOST" if [ -d "$live_dir" ]; then needs_cleanup=false if openssl x509 -in "$cert_file" -noout -issuer 2>/dev/null | grep -q "STAGING"; then echo "Existing staging certificate detected; replacing with a production certificate." needs_cleanup=true elif [ ! -L "$cert_file" ] || [ ! -L "$key_file" ]; then echo "Broken certificate symlinks detected; removing stale data for a fresh issuance." needs_cleanup=true fi if [ "$needs_cleanup" = "true" ]; then FORCE_RENEWAL=true rm -rf "$live_dir" \ "$ROOT_DIR/docker/ssl/letsencrypt/archive/$HOST" \ "$ROOT_DIR/docker/ssl/letsencrypt/renewal/$HOST.conf" fi fi fi echo "Configuring docker/.env for $HOST" set_env MG_RELEASE_TAG latest set_env MG_PUBLIC_HOST "$HOST" set_env MG_UI_HOST "${MG_UI_HOST:-ui}" set_env MG_LETSENCRYPT_ENABLED "$LETSENCRYPT_ENABLED" set_env MG_LETSENCRYPT_EMAIL "$EMAIL" set_env MG_LETSENCRYPT_STAGING "$STAGING" set_env MG_LETSENCRYPT_FORCE_RENEWAL "$FORCE_RENEWAL" set_env MG_NGINX_SERVER_NAME "$HOST" comment_env_any MG_NGINX_SERVER_CERT comment_env_any MG_NGINX_SERVER_KEY set_env MG_UI_DOCKER_ACCEPT_EULA yes set_env MG_OAUTH_UI_REDIRECT_URL "https://$HOST/api/auth/token" set_env MG_OAUTH_UI_ERROR_URL "https://$HOST/login" set_env MG_PASSWORD_RESET_URL_PREFIX "https://$HOST/password-reset" set_env MG_VERIFICATION_URL_PREFIX "https://$HOST/verify-email" set_env MG_GOOGLE_REDIRECT_URL "https://$HOST/oauth/callback/google" set_env NEXTAUTH_URL "https://$HOST" set_env MG_HOST_URL "https://$HOST" set_env MG_UI_BASEURL "https://$HOST" set_env MG_UI_CLI_MQTT_HOST "$HOST" set_env MG_UI_CLI_WS_URL "wss://$HOST/mqtt" set_env MG_UI_CLI_COAP_HOST "$HOST" set_env MG_UI_CLI_HTTP_URL "https://$HOST/http" mkdir -p "$ROOT_DIR/docker/ssl/letsencrypt" "$ROOT_DIR/docker/ssl/certbot-www" write_ui_proxy if [ "$LETSENCRYPT_ENABLED" = "false" ]; then echo "Starting Magistrala with the fallback Nginx certificate" MG_UI_DOCKER_ACCEPT_EULA=yes compose up -d MG_UI_DOCKER_ACCEPT_EULA=yes COMPOSE_PROFILES=letsencrypt compose stop certbot >/dev/null 2>&1 || true echo "Let's Encrypt disabled. Nginx cert/key paths are commented in docker/.env." echo "Fallback TLS setup complete: https://$HOST/" exit 0 fi echo "Starting Magistrala with the fallback Nginx certificate" MG_UI_DOCKER_ACCEPT_EULA=yes compose up -d wait_for_nginx_http echo "Requesting Let's Encrypt certificate for $HOST" MG_UI_DOCKER_ACCEPT_EULA=yes COMPOSE_PROFILES=letsencrypt compose up -d --force-recreate certbot cert_ready() { compose logs certbot 2>&1 | \ grep -qE "Successfully received certificate|Certificate not yet due for renewal" } elapsed=0 until cert_ready || [ "$elapsed" -ge "$TIMEOUT_SECONDS" ]; do compose logs --tail 3 certbot 2>&1 | sed 's/^/ [certbot] /' sleep 5 elapsed=$((elapsed + 5)) done if ! cert_ready; then echo "Timed out waiting for Let's Encrypt certificate." >&2 compose logs --tail 80 certbot >&2 || true exit 1 fi echo "Certificate obtained. Switching Nginx to the issued certificate." set_env MG_NGINX_SERVER_CERT "$cert_path" set_env MG_NGINX_SERVER_KEY "$key_path" set_env MG_LETSENCRYPT_FORCE_RENEWAL false MG_UI_DOCKER_ACCEPT_EULA=yes compose up -d --force-recreate nginx echo "TLS setup complete: https://$HOST/"