feat: Add Caddy configuration and Docker setup for hosting the application

This commit is contained in:
Marco Gallegos
2025-11-24 20:05:26 -06:00
parent 3a4726305d
commit 227f887c78
3 changed files with 262 additions and 0 deletions

41
Caddyfile Normal file
View File

@@ -0,0 +1,41 @@
# Configuración básica de Caddy para solu23.cloud
solu23.cloud {
# Raíz del sitio
root * /var/www/sol23_placeholder
# Habilitar compresión
encode gzip
# Servir archivos estáticos
file_server
# Página de error personalizada (opcional)
handle_errors {
respond "{http.error.status_code} {http.error.status_text}"
}
# Headers de seguridad
header {
# Habilitar HSTS
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
# Prevenir clickjacking
X-Frame-Options "SAMEORIGIN"
# Prevenir MIME sniffing
X-Content-Type-Options "nosniff"
# XSS Protection
X-XSS-Protection "1; mode=block"
# Referrer Policy
Referrer-Policy "strict-origin-when-cross-origin"
}
# Logs (opcional)
log {
output file /var/log/caddy/solu23.log
format json
}
}
# Redirección de www a no-www (opcional)
www.solu23.cloud {
redir https://solu23.cloud{uri} permanent
}

33
docker-compose.yml Normal file
View File

@@ -0,0 +1,33 @@
version: '3.8'
services:
caddy:
image: caddy:2-alpine
container_name: caddy_solu23
restart: unless-stopped
ports:
- "80:80"
- "443:443"
- "443:443/udp" # HTTP/3
volumes:
# Monta el Caddyfile
- ./Caddyfile:/etc/caddy/Caddyfile
# Monta los archivos del sitio
- ./:/var/www/sol23_placeholder
# Datos persistentes de Caddy (certificados SSL)
- caddy_data:/data
- caddy_config:/config
# Logs
- ./logs:/var/log/caddy
networks:
- caddy_network
volumes:
caddy_data:
driver: local
caddy_config:
driver: local
networks:
caddy_network:
driver: bridge

188
health_checker Normal file
View File

@@ -0,0 +1,188 @@
#!/usr/bin/env python3
import requests
import json
from datetime import datetime, timezone
import os
import subprocess # <-- CAMBIO: Importar para ejecutar comandos del sistema
# --- Variables y Configuración ---
webhook_env_var = os.getenv('WEBHOOK_URLS', '')
WEBHOOK_URLS = [url.strip() for url in webhook_env_var.split(',') if url.strip()]
INTERNOS = {
"vps_soul23": "31.97.41.188", # <-- CAMBIO: Añadido el VPS para el ping
"coolify": "https://aperture.soul23.cloud",
"formbricks": "https://feedback.soul23.cloud",
"vikunja": "https://tasks.soul23.cloud",
"gokapi": "https://wetrans.soul23.cloud",
"n8n_flows": "https://flows.soul23.cloud",
"ap_pos": "https://apos.soul23.cloud"
}
EXTERNOS = {
"openai": "https://status.openai.com/",
"google_gemini": "https://aistudio.google.com/status",
"canva": "https://www.canvastatus.com/",
"tiktok": "https://www.tiktok.com",
"facebook": "https://www.facebook.com",
"instagram": "https://www.instagram.com",
"telegram": "https://core.telegram.org",
"whatsapp": "https://www.whatsapp.com",
"x": "https://www.x.com",
"youtube": "https://www.youtube.com"
}
STATUSPAGE_SERVICES = ["openai", "canva"]
# --- Funciones de Verificación de Estado ---
def check_url(url):
try:
r = requests.get(url, timeout=8)
return r.status_code
except:
return 0
# <-- CAMBIO: Nueva función para hacer ping al VPS
def check_vps_ping(ip_address):
"""
Envía un único paquete ICMP (ping) a la IP especificada.
Devuelve un estado basado en si el host respondió.
"""
try:
# Comando de ping para Linux (que es lo que usan los runners de GitHub):
# -c 1: Enviar solo 1 paquete.
# -W 2: Esperar un máximo de 2 segundos por una respuesta.
command = ["ping", "-c", "1", "-W", "2", ip_address]
# Ejecuta el comando, ocultando la salida para mantener los logs limpios.
result = subprocess.run(command, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
# Un código de retorno 0 significa que el ping fue exitoso.
if result.returncode == 0:
return f"🟢 OK (VPS Reachable)"
else:
return f"🔴 Caído (VPS Unreachable)"
except Exception:
# Esto podría pasar si el comando 'ping' no estuviera disponible.
return f"🔴 Error (Ping command failed)"
def check_formbricks_health(base_url):
health_url = f"{base_url.rstrip('/')}/health"
try:
response = requests.get(health_url, timeout=8)
if response.status_code == 200:
try:
data = response.json()
if data.get("status") == "ok":
return f"🟢 OK (API Health: ok)"
else:
return f"🟡 Advertencia (API Health: {data.get('status', 'unknown')})"
except json.JSONDecodeError:
return "🟡 Advertencia (Respuesta no es JSON válido)"
else:
return f"🔴 Caído (Health Endpoint: {response.status_code})"
except requests.RequestException as e:
return f"🔴 Caído (Error de red: {e})"
def get_statuspage_status(base_url):
api_url = f"{base_url.rstrip('/')}/api/v2/summary.json"
try:
response = requests.get(api_url, timeout=8)
if response.status_code == 200:
data = response.json()
status_description = data.get('status', {}).get('description')
if status_description:
status_indicator = data.get('status', {}).get('indicator')
if status_indicator == 'none':
return f"🟢 OK ({status_description})"
else:
return f"🟡 Advertencia ({status_description})"
return "🟡 Advertencia (Respuesta JSON inesperada)"
else:
return f"🔴 Caído (API Status: {response.status_code})"
except requests.RequestException as e:
return f"🔴 Caído (Error de red: {e})"
except json.JSONDecodeError:
return f"🔴 Caído (No se pudo decodificar JSON)"
def get_gemini_status(status_url):
incidents_url = "https://status.cloud.google.com/incidents.json"
try:
response = requests.get(incidents_url, timeout=8)
if response.status_code == 200:
incidents = response.json()
gemini_incident = any('Vertex AI' in i.get('service_name', '') and i.get('end') is None for i in incidents)
if not gemini_incident:
return "🟢 OK (Sin incidentes reportados para Vertex AI)"
else:
return "🟡 Advertencia (Incidente activo en Vertex AI)"
else:
return f"🔴 Caído ({response.status_code})"
except requests.RequestException as e:
return f"🔴 Caído (Error: {e})"
def human_state(code):
if code == 200:
return f"🟢 OK ({code})"
if code in (301, 302, 307, 308, 401, 403, 404):
return f"🟡 Advertencia ({code})"
return f"🔴 Caído ({code})"
# --- Lógica Principal ---
def build_section(diccionario):
salida = {}
for nombre, url_or_ip in diccionario.items():
# <-- CAMBIO: Añadida la lógica para el ping del VPS
if nombre == 'vps_soul23':
status_message = check_vps_ping(url_or_ip)
salida[f"{nombre}_url"] = url_or_ip # Guardamos la IP
elif nombre in STATUSPAGE_SERVICES:
status_message = get_statuspage_status(url_or_ip)
elif nombre == 'google_gemini':
status_message = get_gemini_status(url_or_ip)
elif nombre == 'formbricks':
status_message = check_formbricks_health(url_or_ip)
else:
status = check_url(url_or_ip)
salida[f"{nombre}_url"] = url_or_ip
salida[f"{nombre}_status"] = status
salida[f"{nombre}_state"] = human_state(status)
continue
# Asignación para los casos especiales
salida[f"{nombre}_url"] = url_or_ip
salida[f"{nombre}_status"] = status_message
salida[f"{nombre}_state"] = status_message
return salida
def main():
if not WEBHOOK_URLS:
print("⚠️ Advertencia: La variable de entorno WEBHOOK_URLS no está configurada o está vacía.")
resultado = {
"timestamp": datetime.now(timezone.utc).isoformat(),
"internos": build_section(INTERNOS),
"externos": build_section(EXTERNOS)
}
print("\n--- Enviando a Webhooks ---")
for url in WEBHOOK_URLS:
try:
requests.post(url, json=resultado, timeout=10)
print(f"✅ Resultado enviado exitosamente a: {url}")
except requests.RequestException as e:
print(f"❌ Error al enviar al webhook {url}: {e}")
print("\n--- Payload Enviado ---")
print(json.dumps(resultado, indent=4))
if __name__ == "__main__":
main()