#!/usr/bin/env bash # ============================================================================= # setup-domains.sh — Script de Automatización de Dominios (Cloudflare y Dokploy) # ============================================================================= # # Propósito: # Este script automatiza el flujo completo de enrutamiento y despliegue de # los subdominios requeridos por la plataforma OmniOil en un entorno de producción. # # Realiza dos tareas principales: # 1. Crea o actualiza registros DNS tipo "A" en Cloudflare apuntando a la # IP pública del servidor VPS, con el Proxy (CDN) activo. # 2. Registra los dominios en la API de Dokploy asociándolos a cada servicio # específico dentro de la pila Docker Compose (con soporte SSL automático # mediante Let's Encrypt), e inicia un re-despliegue de la pila para aplicar # los cambios de enrutamiento en Traefik. # # Requisitos del Sistema: # - curl: Para realizar peticiones HTTP/REST a las APIs. # - jq: Para parsear e inyectar payloads JSON. El script tiene un buscador # inteligente de jq en rutas locales (por si no está instalado a nivel global). # # Autenticación y Credenciales: # Las credenciales se pueden pasar por línea de comandos o configurar en un # archivo .env o .env.production local (el script los carga automáticamente). # # ============================================================================= set -euo pipefail # ----------------------------------------------------------------------------- # 1. Configuración por Defecto y Variables de Estado # ----------------------------------------------------------------------------- DOMAIN="" # Dominio base (ej. omnioil.app). Se solicita de forma interactiva si falta. SERVER_IP="" # IP pública del VPS. Se solicita de forma interactiva si falta. PROJECT_NAME="omnioil"# Nombre del proyecto dentro del panel de Dokploy. COMPOSE_NAME="anh" # Nombre del Docker Compose en Dokploy (corresponde a la pila unificada). CF_TOKEN="" # Token de API de Cloudflare con permisos de edición de registros DNS. CF_ZONE_ID="" # ID de la Zona de Cloudflare asignada al dominio. DOKPLOY_API_URL="" # URL base de la API de Dokploy. DOKPLOY_API_KEY="" # Token/API Key generada en el perfil de administrador de Dokploy. DRY_RUN=false # Si es true, corre en modo de simulación imprimiendo las acciones. SKIP_REDEPLOY=false # Si es true, omite el comando de redeploy en Dokploy al finalizar. CLOUDFLARE_API="https://api.cloudflare.com/client/v4" # ----------------------------------------------------------------------------- # 2. Función de Carga Segura de Archivos de Entorno (.env) # ----------------------------------------------------------------------------- # Esta función lee un archivo .env línea por línea de forma nativa en Bash. # A diferencia de un "source" o "eval" directo en el archivo completo, esta lógica: # - Evita la expansión accidental de caracteres comodín (*) comunes en expresiones # cron como BACKUP_SCHEDULE="0 3 * * *". # - Remueve de manera segura los comentarios al final de la línea. # - Limpia comillas dobles y simples exteriores de los valores. load_env_file() { local env_file="$1" if [ -f "$env_file" ]; then echo "ℹ️ Cargando variables de entorno desde $env_file..." while IFS= read -r line || [ -n "$line" ]; do # Eliminar espacios en blanco al principio de la línea line="${line##[[:space:]]}" # Saltar comentarios y líneas vacías [[ "$line" =~ ^# ]] && continue [[ -z "$line" ]] && continue # Procesar solo líneas que contengan una asignación '=' if [[ "$line" == *=* ]]; then local key="${line%%=*}" local value="${line#*=}" # Quitar comentarios al final del valor (delimitados por '#') value="${value%%#*}" # Recortar espacios sobrantes al inicio y final en Bash puro (sin subshells) key="${key#"${key%%[![:space:]]*}"}" key="${key%"${key##*[![:space:]]}"}" value="${value#"${value%%[![:space:]]*}"}" value="${value%"${value##*[![:space:]]}"}" # Quitar comillas exteriores del valor si las tuviera if [[ "$value" =~ ^\".*\"$ ]] || [[ "$value" =~ ^\'.*\'$ ]]; then value="${value:1:${#value}-2}" fi # Exportar la variable al entorno actual usando declare (seguro contra globbing) if [ -n "$key" ]; then declare -x "$key=$value" 2>/dev/null || true fi fi done < "$env_file" fi } # Cargar configuración desde archivos locales load_env_file ".env" load_env_file ".env.production" # Acoplar variables precargadas desde el entorno o archivos .env DOMAIN="${DOMAIN:-${OMNIOIL_DOMAIN:-}}" SERVER_IP="${SERVER_IP:-${OMNIOIL_IP:-}}" CF_TOKEN="${CF_TOKEN:-}" CF_ZONE_ID="${CF_ZONE_ID:-}" DOKPLOY_API_URL="${DOKPLOY_API_URL:-}" DOKPLOY_API_KEY="${DOKPLOY_API_KEY:-}" # ----------------------------------------------------------------------------- # 3. Procesamiento de Argumentos de la Línea de Comandos # ----------------------------------------------------------------------------- # Las opciones pasadas por la terminal tienen prioridad sobre los archivos .env. while [[ $# -gt 0 ]]; do case "$1" in --domain) DOMAIN="$2" shift 2 ;; --ip) SERVER_IP="$2" shift 2 ;; --project) PROJECT_NAME="$2" shift 2 ;; --compose) COMPOSE_NAME="$2" shift 2 ;; --cf-token) CF_TOKEN="$2" shift 2 ;; --cf-zone-id) CF_ZONE_ID="$2" shift 2 ;; --dokploy-url) DOKPLOY_API_URL="$2" shift 2 ;; --dokploy-key) DOKPLOY_API_KEY="$2" shift 2 ;; --dry-run) DRY_RUN=true shift ;; --skip-redeploy) SKIP_REDEPLOY=true shift ;; -h|--help) echo "Uso: $0 [opciones]" echo "" echo "Opciones:" echo " --domain Dominio principal (ej. omnioil.app)" echo " --ip IP pública del servidor VPS" echo " --project Nombre del proyecto en Dokploy (default: omnioil)" echo " --compose Nombre de la app Compose en Dokploy (default: anh)" echo " --cf-token Token de Cloudflare API (permiso DNS Edit)" echo " --cf-zone-id Zone ID de Cloudflare" echo " --dokploy-url URL API Dokploy (default: http://:3000/api)" echo " --dokploy-key API Key de Dokploy" echo " --dry-run Modo simulación (imprime acciones sin hacer cambios reales)" echo " --skip-redeploy Evita el re-despliegue automático en Dokploy" exit 0 ;; *) echo "❌ Opción desconocida: $1" echo "Ejecuta '$0 --help' para ver las opciones disponibles." exit 1 ;; esac done # ----------------------------------------------------------------------------- # 4. Validaciones de Entorno e Interactividad # ----------------------------------------------------------------------------- # El comando 'curl' es indispensable para las peticiones de red if ! command -v curl &>/dev/null; then echo "❌ Error: Se requiere 'curl' instalado en el sistema." exit 1 fi # Buscador tolerante de 'jq' # Si no está instalado globalmente en el sistema, busca en carpetas de usuario # locales de WSL o rutas del proyecto para mitigar problemas de dependencias. JQ_CMD="jq" if ! command -v jq &>/dev/null; then if [ -f "/home/yesid_farfan/jq" ]; then JQ_CMD="/home/yesid_farfan/jq" elif [ -f "$HOME/jq" ]; then JQ_CMD="$HOME/jq" elif [ -f "./jq" ]; then JQ_CMD="./jq" elif [ -f "./scripts/jq" ]; then JQ_CMD="./scripts/jq" else echo "❌ Error: Se requiere 'jq' instalado en el sistema para procesar respuestas JSON." exit 1 fi fi # Solicitar dominio e IP al usuario interactivamente si no están configurados if [ -z "$DOMAIN" ]; then read -rp "Introduce el dominio principal (ej. omnioil.app): " DOMAIN fi if [ -z "$SERVER_IP" ]; then read -rp "Introduce la IP pública del servidor VPS: " SERVER_IP fi # Cancelar si faltan los datos mínimos obligatorios if [ -z "$DOMAIN" ] || [ -z "$SERVER_IP" ]; then echo "❌ Error: El dominio y la IP del servidor son campos obligatorios." exit 1 fi # Asignar la URL de la API de Dokploy por defecto a partir de la IP del VPS si no se provee if [ -z "$DOKPLOY_API_URL" ]; then DOKPLOY_API_URL="http://$SERVER_IP:3000/api" fi # Evaluar credenciales para determinar qué plataformas se van a configurar RUN_CLOUDFLARE=true if [ -z "$CF_TOKEN" ] || [ -z "$CF_ZONE_ID" ]; then echo "⚠️ Advertencia: Falta CF_TOKEN o CF_ZONE_ID. Se omitirá Cloudflare." RUN_CLOUDFLARE=false fi RUN_DOKPLOY=true if [ -z "$DOKPLOY_API_KEY" ]; then echo "⚠️ Advertencia: Falta DOKPLOY_API_KEY. Se omitirá Dokploy." RUN_DOKPLOY=false fi # Si no hay credenciales para ninguna plataforma, salimos if [ "$RUN_CLOUDFLARE" = false ] && [ "$RUN_DOKPLOY" = false ]; then echo "❌ Error: No hay credenciales configuradas para Cloudflare ni para Dokploy." exit 1 fi # ----------------------------------------------------------------------------- # 5. Mapeo de Subdominios a Contenedores Docker Compose # ----------------------------------------------------------------------------- # Formato: "subdominio:nombre_servicio_docker:puerto_interno" # Este mapeo se deriva del documento docs/DESPLIEGUE.md. MAPPINGS=( "@:landing:80" # omnioil.app -> contenedor landing puerto 80 "www:landing:80" # www.omnioil.app -> contenedor landing puerto 80 "app:frontend-admin:80" # app.omnioil.app -> contenedor frontend-admin puerto 80 "console:frontend-console:80" # console.omnioil.app -> contenedor frontend-console puerto 80 "mobile:frontend-pwa:80" # mobile.omnioil.app -> contenedor frontend-pwa puerto 80 "api:backend-api:8000" # api.omnioil.app -> contenedor backend-api puerto 8000 "storage:minio:9001" # storage.omnioil.app -> consola de administración de minio puerto 9001 "mqtt:mosquitto:9001" # mqtt.omnioil.app -> broker websocket mosquitto puerto 9001 ) # ----------------------------------------------------------------------------- # 6. Funciones de Integración con la API de Cloudflare (DNS) # ----------------------------------------------------------------------------- # Crea o actualiza un registro DNS tipo A en Cloudflare setup_cloudflare_dns() { local name="$1" content="$2" type="${3:-A}" proxied="${4:-true}" local record_name # Manejar el dominio raíz (@) versus subdominios if [ "$name" = "@" ]; then record_name="$DOMAIN" else record_name="$name.$DOMAIN" fi echo " → Configurando DNS para $record_name -> $content" # Buscar si el registro ya existe en el Zone ID local search_res search_res=$(curl -s -X GET "$CLOUDFLARE_API/zones/$CF_ZONE_ID/dns_records?name=$record_name&type=$type" \ -H "Authorization: Bearer $CF_TOKEN" \ -H "Content-Type: application/json") # Validar que la respuesta sea un JSON de éxito if ! echo "$search_res" | "$JQ_CMD" -e '.success' | grep -q "true"; then local err err=$(echo "$search_res" | "$JQ_CMD" -r '.errors[0].message // "Error en autenticación/red"') echo " ❌ Falló la consulta a Cloudflare: $err" return 1 fi # Extraer el record_id si existe local record_id record_id=$(echo "$search_res" | "$JQ_CMD" -r '.result[0].id // empty') # Generar el payload JSON de forma segura local payload payload=$("$JQ_CMD" -n \ --arg type "$type" \ --arg name "$record_name" \ --arg content "$content" \ --argjson proxied "$proxied" \ '{type: $type, name: $name, content: $content, proxied: $proxied, ttl: 1}') local response if [ -n "$record_id" ]; then # Registro existente: Se actualiza con PUT response=$(curl -s -X PUT "$CLOUDFLARE_API/zones/$CF_ZONE_ID/dns_records/$record_id" \ -H "Authorization: Bearer $CF_TOKEN" \ -H "Content-Type: application/json" \ -d "$payload") else # Registro nuevo: Se crea con POST response=$(curl -s -X POST "$CLOUDFLARE_API/zones/$CF_ZONE_ID/dns_records" \ -H "Authorization: Bearer $CF_TOKEN" \ -H "Content-Type: application/json" \ -d "$payload") fi # Verificar resultado de la operación if echo "$response" | "$JQ_CMD" -e '.success' | grep -q "true"; then echo " ✅ DNS $record_name configurado con éxito." else local err err=$(echo "$response" | "$JQ_CMD" -r '.errors[0].message // "Error desconocido"') echo " ❌ Falló la creación/actualización de DNS $record_name: $err" fi } # ----------------------------------------------------------------------------- # 7. Funciones de Integración con la API de Dokploy (Enrutamiento) # ----------------------------------------------------------------------------- # Crea el enrutamiento de un dominio en Dokploy vinculándolo a un servicio del Docker Compose create_dokploy_domain() { local host="$1" service="$2" port="$3" compose_id="$4" echo " → Registrando en Dokploy: $host -> servicio '$service' (puerto $port)" # payload JSON para enrutar el tráfico al contenedor mediante Traefik. # Se habilita SSL Let's Encrypt de forma automática (https: true, certificateType: letsencrypt) local payload payload=$("$JQ_CMD" -n \ --arg host "$host" \ --arg composeId "$compose_id" \ --arg serviceName "$service" \ --argjson port "$port" \ '{host: $host, composeId: $composeId, serviceName: $serviceName, port: $port, https: true, certificateType: "letsencrypt", path: "/"}') local response response=$(curl -s -X POST "$DOKPLOY_API_URL/domain.create" \ -H "x-api-key: $DOKPLOY_API_KEY" \ -H "Content-Type: application/json" \ -d "$payload") # Evaluar respuestas de error if echo "$response" | "$JQ_CMD" -e '.error // .message // empty' > /dev/null; then local error_msg error_msg=$(echo "$response" | "$JQ_CMD" -r '.message // .error') # Si el dominio ya existe, el script es tolerante y no falla, simplemente salta al siguiente if [[ "$error_msg" == *"already exists"* || "$error_msg" == *"Duplicate"* ]]; then echo " ⚠️ El dominio $host ya está registrado en Dokploy. Saltando..." else echo " ❌ Error al registrar dominio $host en Dokploy: $error_msg" fi else echo " ✅ Dominio $host enrutado con éxito." fi } # Envía una señal de re-despliegue (redeploy) al Docker Compose en Dokploy. # Esto es mandatorio porque Dokploy usa Traefik con enrutamiento por etiquetas (labels). # Al añadir dominios a Compose, las etiquetas cambian y los contenedores deben recrearse. redeploy_dokploy_compose() { local compose_id="$1" echo "=== Solicitando Re-despliegue de la pila en Dokploy ===" local response response=$(curl -s -X POST "$DOKPLOY_API_URL/compose.redeploy" \ -H "x-api-key: $DOKPLOY_API_KEY" \ -H "Content-Type: application/json" \ -d "{\"composeId\":\"$compose_id\"}") if echo "$response" | "$JQ_CMD" -e '.error // .message // empty' > /dev/null; then local error_msg error_msg=$(echo "$response" | "$JQ_CMD" -r '.message // .error') echo " ❌ Error al solicitar re-despliegue: $error_msg" else echo " ✅ Re-despliegue iniciado con éxito en Dokploy." echo " Traefik configurará SSL Let's Encrypt automáticamente una vez estén listos los servicios." fi } # ============================================================================= # 8. Ejecución del Script (Flujo de Control) # ============================================================================= echo "=========================================================" echo " AUTOMATIZACIÓN DE DOMINIOS Y ENRUTAMIENTO" echo " Dominio: $DOMAIN" echo " IP Servidor: $SERVER_IP" if [ "$DRY_RUN" = true ]; then echo " ⚠️ MODO SIMULACIÓN ACTIVO (No se harán cambios reales)" fi echo "=========================================================" # ----------------------------------------------------------------------------- # Paso 1: Configurar DNS en Cloudflare # ----------------------------------------------------------------------------- if [ "$RUN_CLOUDFLARE" = true ]; then echo "" echo "=== Paso 1: Configurando registros DNS en Cloudflare ===" for mapping in "${MAPPINGS[@]}"; do # Separar el mapeo en variables IFS=":" read -r sub service port <<< "$mapping" record_name="" if [ "$sub" = "@" ]; then record_name="$DOMAIN"; else record_name="$sub.$DOMAIN"; fi if [ "$DRY_RUN" = true ]; then echo " [SIMULACIÓN] Crear/Actualizar A: $record_name -> $SERVER_IP (proxied=true)" else setup_cloudflare_dns "$sub" "$SERVER_IP" "A" true fi done fi # ----------------------------------------------------------------------------- # Paso 2: Resolución de recursos e IDs en Dokploy # ----------------------------------------------------------------------------- if [ "$RUN_DOKPLOY" = true ]; then echo "" echo "=== Paso 2: Conectando con Dokploy y buscando recursos ===" compose_id="dummy-compose-id" if [ "$DRY_RUN" = false ]; then # 2a. Obtener la lista de todos los proyectos de la instancia projects_json=$(curl -s -f -X GET "$DOKPLOY_API_URL/project.all" -H "x-api-key: $DOKPLOY_API_KEY") if [ $? -ne 0 ] || [ -z "$projects_json" ]; then echo "❌ Error: No se pudo conectar a Dokploy en $DOKPLOY_API_URL o la API key es inválida." exit 1 fi # 2b. Filtrar para hallar el projectId que coincida con PROJECT_NAME project_id=$(echo "$projects_json" | "$JQ_CMD" -r --arg name "$PROJECT_NAME" '.[] | select(.name == $name) | .projectId // empty') if [ -z "$project_id" ]; then echo "❌ Error: El proyecto '$PROJECT_NAME' no fue encontrado en Dokploy." echo "Proyectos detectados en Dokploy:" echo "$projects_json" | "$JQ_CMD" -r '.[].name' exit 1 fi echo " ✅ Proyecto '$PROJECT_NAME' encontrado. (ID: $project_id)" # 2c. Obtener los detalles del proyecto para buscar el recurso Compose project_details=$(curl -s -f -X GET "$DOKPLOY_API_URL/project.one?projectId=$project_id" -H "x-api-key: $DOKPLOY_API_KEY") # 2d. Filtrar para hallar el composeId o id del recurso Compose compose_id=$(echo "$project_details" | "$JQ_CMD" -r --arg name "$COMPOSE_NAME" '.compose[] | select(.name == $name) | .composeId // .id // empty') if [ -z "$compose_id" ]; then echo "❌ Error: El recurso Docker Compose '$COMPOSE_NAME' no fue encontrado en el proyecto '$PROJECT_NAME'." echo "Compose apps detectadas:" echo "$project_details" | "$JQ_CMD" -r '.compose[].name // "Ninguno"' exit 1 fi echo " ✅ Docker Compose '$COMPOSE_NAME' encontrado. (ID: $compose_id)" else echo " [SIMULACIÓN] Buscado proyecto '$PROJECT_NAME' y Compose '$COMPOSE_NAME' (IDs simulados)." fi # ----------------------------------------------------------------------------- # Paso 3: Registrar dominios en la API de Dokploy # ----------------------------------------------------------------------------- echo "" echo "=== Paso 3: Registrando dominios en Dokploy ===" for mapping in "${MAPPINGS[@]}"; do # Separar mapeo IFS=":" read -r sub service port <<< "$mapping" host="" if [ "$sub" = "@" ]; then host="$DOMAIN" else host="$sub.$DOMAIN" fi if [ "$DRY_RUN" = true ]; then echo " [SIMULACIÓN] Registrar dominio: $host -> servicio '$service' (puerto $port)" else create_dokploy_domain "$host" "$service" "$port" "$compose_id" fi done # ----------------------------------------------------------------------------- # Paso 4: Re-despliegue de la pila para aplicar cambios en Traefik # ----------------------------------------------------------------------------- if [ "$SKIP_REDEPLOY" = false ]; then echo "" if [ "$DRY_RUN" = true ]; then echo " [SIMULACIÓN] Re-desplegar compose con ID $compose_id" else redeploy_dokploy_compose "$compose_id" fi fi fi echo "" echo "=========================================================" echo " 🎉 PROCESO FINALIZADO CON ÉXITO" echo "========================================================="