auth (HTML importado)

deploy (HTML importado)

<!doctype html><meta charset="utf-8"><title>Convenciones & Deploy</title>
<link rel="stylesheet" href="data:text/css,body{font-family:system-ui;margin:0 auto;max-width:900px;padding:2rem;line-height:1.55}pre,code{background:#f5f5f5;padding:.2em .4em;border-radius:4px}pre{padding:1rem;overflow:auto}h1,h2{margin-top:2rem}">
<nav style="display:flex;gap:.5rem;flex-wrap:wrap;margin:0 0 1rem">
  <a href="/">Estado</a><a href="/docs/">Docs</a><a href="/docs/mapa.html">Mapa</a>
  <a href="/grafana/">Grafana</a><a href="/prometheus/">Prometheus</a>
  <a href="/netdata/">Netdata</a><a href="/status.json">status.json</a>
  <a href="/docs/copiar.html"><b>Copiar TODOS</b></a>
</nav>
<h1>Convenciones & Deploy</h1>
<ul>
  <li>Estructura: <code>/srv/projects/&lt;slug&gt;/</code> con <code>venv/</code>, <code>.env</code>, <code>run.py</code>, <code>deploy/</code>.</li>
  <li>Slugs: <code>dominio-subdominio</code>. Puertos apps 8101–8199; infra ≥ 9000.</li>
  <li><b>run.py</b> autodetecta: <code>wsgi:app</code> (gunicorn), <code>main:app</code> (uvicorn/FastAPI) o <code>app:app</code> (Flask). Fallback expone <code>/health</code>.</li>
  <li><b>systemd</b>: <code>/etc/systemd/system/&lt;slug&gt;.service</code>, <code>ExecStart</code> usa <code>venv/bin/python run.py</code>.</li>
  <li><b>Caddy</b>: vhosts con snippet <code>(tls_cf)</code> (DNS-01 Cloudflare) y health.</li>
  <li>Monitor: <code>infra.illanes00.cl</code> + <code>/status.json</code>.</li>
</ul>
<h2>Runbook de deploy rápido</h2>
<pre><code># crear proyecto
sudo mkdir -p /srv/projects/&lt;slug&gt; && cd /srv/projects/&lt;slug&gt;
python3 -m venv venv
echo PORT=&lt;puerto&gt; &gt; .env
venv/bin/pip install --upgrade pip gunicorn uvicorn fastapi flask python-dotenv

## run.py estándar (módulos → respeta venv)
cat &gt; run.py &lt;&lt;'PY'
#!/usr/bin/env python3
import os, sys, importlib, subprocess
from pathlib import Path
try:
  from dotenv import load_dotenv; load_dotenv(".env")
except Exception:
  pass
HOST=os.getenv("HOST","127.0.0.1"); PORT=os.getenv("PORT","8101")
exists=lambda p: Path(p).exists()
if exists("wsgi.py"):
  sys.exit(subprocess.call([sys.executable,"-m","gunicorn",f"--bind={HOST}:{PORT}","wsgi:app"]))
if exists("app/main.py") or exists("main.py"):
  target="app.main:app" if exists("app/main.py") else "main:app"
  try:
    importlib.import_module(target.split(":")[0])
    sys.exit(subprocess.call([sys.executable,"-m","uvicorn",target,"--host",HOST,"--port",PORT]))
  except Exception: pass
if exists("app.py"):
  try:
    appmod=importlib.import_module("app")
    if getattr(appmod,"app",None) is not None:
      sys.exit(subprocess.call([sys.executable,"-m","gunicorn",f"--bind={HOST}:{PORT}","app:app"]))
  except Exception: pass
from flask import Flask
app=Flask(__name__)
@app.get("/health")
def h(): return {"ok":True}
if __name__=="__main__": app.run(host=HOST, port=int(PORT))
PY
chmod +x run.py

## systemd
sudo tee /etc/systemd/system/&lt;slug&gt;.service &gt;/dev/null &lt;&lt;'UNIT'
[Unit]
Description=%i service (run.py)
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=%i
WorkingDirectory=/srv/projects/%i
EnvironmentFile=-/srv/projects/%i/.env
ExecStart=/srv/projects/%i/venv/bin/python /srv/projects/%i/run.py
Restart=on-failure
StartLimitIntervalSec=120
StartLimitBurst=10
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=full

[Install]
WantedBy=multi-user.target
UNIT

sudo systemctl daemon-reload
sudo systemctl enable --now &lt;slug&gt;

## Caddy vhost (ejemplo)
sudo tee /etc/caddy/sites.d/&lt;slug&gt;.caddy &gt;/dev/null &lt;&lt;'CADDY'
dominio.example.com {
  import tls_cf
  encode zstd gzip
  @health path /health
  handle_path /health* {
    reverse_proxy 127.0.0.1:8101
  }
  reverse_proxy 127.0.0.1:8101
}
CADDY
sudo caddy reload --config /etc/caddy/Caddyfile
</code></pre>

proyectos (HTML importado)

<!doctype html><meta charset="utf-8"><title>Crear un nuevo proyecto</title>
<link rel="stylesheet" href="data:text/css,body{font-family:system-ui;margin:0 auto;max-width:900px;padding:2rem;line-height:1.5}pre,code{background:#f5f5f5;padding:.2em .4em;border-radius:4px}pre{padding:1rem;overflow:auto}">
<nav style="display:flex;gap:.5rem;flex-wrap:wrap;margin:0 0 1rem">
  <a href="/">Estado</a><a href="/docs/">Docs</a><a href="/docs/mapa.html">Mapa</a>
  <a href="/grafana/">Grafana</a><a href="/prometheus/">Prometheus</a>
  <a href="/netdata/">Netdata</a><a href="/status.json">status.json</a>
  <a href="/docs/copiar.html"><b>Copiar TODOS</b></a>
</nav>
<h1>Crear un nuevo proyecto</h1>
<ol>
  <li>Elegir <b>slug</b> y <b>puerto</b> libre (8101–8199).</li>
  <li>Crear carpeta + venv + <code>.env</code> + <code>run.py</code>.</li>
  <li>Instalar deps y definir <code>/health</code>.</li>
  <li>Crear unit systemd + vhost Caddy.</li>
  <li>Smoke tests local y público.</li>
</ol>
<pre><code>sudo mkdir -p /srv/projects/&lt;slug&gt; && cd /srv/projects/&lt;slug&gt;
python3 -m venv venv
echo PORT=&lt;puerto&gt; &gt; .env
venv/bin/pip install fastapi uvicorn gunicorn flask python-dotenv
## (pegar run.py estándar de Deploy)
</code></pre>

backups (HTML importado)

<!doctype html><meta charset="utf-8"><title>Backups & DB</title>
<link rel="stylesheet" href="data:text/css,body{font-family:system-ui;margin:0 auto;max-width:900px;padding:2rem;line-height:1.5}pre,code{background:#f5f5f5;padding:.2em .4em;border-radius:4px}pre{padding:1rem;overflow:auto}">
<nav style="display:flex;gap:.5rem;flex-wrap:wrap;margin:0 0 1rem">
  <a href="/">Estado</a><a href="/docs/">Docs</a><a href="/docs/mapa.html">Mapa</a>
  <a href="/grafana/">Grafana</a><a href="/prometheus/">Prometheus</a>
  <a href="/netdata/">Netdata</a><a href="/status.json">status.json</a>
  <a href="/docs/copiar.html"><b>Copiar TODOS</b></a>
</nav>
<h1>Backups & Base de datos</h1>
<ul>
  <li>PostgreSQL central (futuro PgBouncer).</li>
  <li>Dumps diarios locales + (opcional) subida a S3.</li>
  <li>Retención sugerida: 7 días locales + 30 en remoto.</li>
</ul>
<h2>Dump manual</h2>
<pre>PGPASSWORD=*** pg_dump -h localhost -U user dbname | gzip &gt; /var/backups/dbname-$(date +%F).sql.gz</pre>

status (HTML importado)

<!doctype html><meta charset="utf-8"><title>status.json</title>
<link rel="stylesheet" href="data:text/css,body{font-family:system-ui;margin:0 auto;max-width:900px;padding:2rem;line-height:1.5}pre,code{background:#f5f5f5;padding:.2em .4em;border-radius:4px}pre{padding:1rem;overflow:auto}">
<nav style="display:flex;gap:.5rem;flex-wrap:wrap;margin:0 0 1rem">
  <a href="/">Estado</a><a href="/docs/">Docs</a><a href="/docs/mapa.html">Mapa</a>
  <a href="/grafana/">Grafana</a><a href="/prometheus/">Prometheus</a>
  <a href="/netdata/">Netdata</a><a href="/status.json">status.json</a>
  <a href="/docs/copiar.html"><b>Copiar TODOS</b></a>
</nav>
<h1>status.json</h1>
<p>Documento JSON consumido por el infra-monitor para pintar el estado. Debe incluir cada servicio con systemd y HTTP (local/público) cuando aplique.</p>

observabilidad (HTML importado)

<!doctype html><meta charset="utf-8"><title>Observabilidad</title>
<link rel="stylesheet" href="data:text/css,body{font-family:system-ui;margin:0 auto;max-width:900px;padding:2rem;line-height:1.5}pre,code{background:#f5f5f5;padding:.2em .4em;border-radius:4px}pre{padding:1rem;overflow:auto}">
<nav style="display:flex;gap:.5rem;flex-wrap:wrap;margin:0 0 1rem">
  <a href="/">Estado</a><a href="/docs/">Docs</a><a href="/docs/mapa.html">Mapa</a>
  <a href="/grafana/">Grafana</a><a href="/prometheus/">Prometheus</a>
  <a href="/netdata/">Netdata</a><a href="/status.json">status.json</a>
  <a href="/docs/copiar.html"><b>Copiar TODOS</b></a>
</nav>
<h1>Observabilidad</h1>
<ul>
  <li><b>Grafana</b>: dashboards y alertas.</li>
  <li><b>Prometheus</b>: scraping de exporters.</li>
  <li><b>Netdata</b>: visión de nodo (CPU, RAM, IO).</li>
</ul>
<h2>Buenas prácticas</h2>
<ul>
  <li>Todo servicio expone <code>/health</code> (200 JSON).</li>
  <li>Etiquetas estándar en métricas: <code>service</code>, <code>slug</code>, <code>env</code>.</li>
  <li>Alertas mínimas: 5xx > 1% 5min; down de systemd; latency p95 > umbral.</li>
</ul>

debugging (HTML importado)

<!doctype html><meta charset="utf-8"><title>Debugging rápido</title>
<link rel="stylesheet" href="data:text/css,body{font-family:system-ui;margin:0 auto;max-width:900px;padding:2rem;line-height:1.5}pre,code{background:#f5f5f5;padding:.2em .4em;border-radius:4px}pre{padding:1rem;overflow:auto}table{border-collapse:collapse;width:100%}td,th{padding:.4rem;border-bottom:1px solid #ddd;text-align:left}">
<nav style="display:flex;gap:.5rem;flex-wrap:wrap;margin:0 0 1rem">
  <a href="/">Estado</a><a href="/docs/">Docs</a><a href="/docs/mapa.html">Mapa</a>
  <a href="/grafana/">Grafana</a><a href="/prometheus/">Prometheus</a>
  <a href="/netdata/">Netdata</a><a href="/status.json">status.json</a>
  <a href="/docs/copiar.html"><b>Copiar TODOS</b></a>
</nav>
<h1>Debugging rápido</h1>
<table>
<tr><th>¿Qué falla?</th><th>Comando</th><th>Pista</th></tr>
<tr><td>Servicio down</td><td><code>systemctl status &lt;slug&gt;</code></td><td>Logs de run.py/gunicorn/uvicorn</td></tr>
<tr><td>HTTP down</td><td><code>curl -I 127.0.0.1:PORT/health</code></td><td>¿Escucha el backend?</td></tr>
<tr><td>502</td><td><code>journalctl -u caddy -n 50</code></td><td>Proxy/backend mal</td></tr>
<tr><td>TLS</td><td><code>caddy validate</code></td><td>Token Cloudflare</td></tr>
<tr><td>Ports</td><td><code>ss -ltnp | grep :PORT</code></td><td>Conflictos</td></tr>
</table>

troubleshooting (HTML importado)

<!doctype html><meta charset="utf-8"><title>Troubleshooting</title>
<link rel="stylesheet" href="data:text/css,body{font-family:system-ui;margin:0 auto;max-width:900px;padding:2rem;line-height:1.5}pre,code{background:#f5f5f5;padding:.2em .4em;border-radius:4px}pre{padding:1rem;overflow:auto}">
<nav style="display:flex;gap:.5rem;flex-wrap:wrap;margin:0 0 1rem">
  <a href="/">Estado</a><a href="/docs/">Docs</a><a href="/docs/mapa.html">Mapa</a>
  <a href="/grafana/">Grafana</a><a href="/prometheus/">Prometheus</a>
  <a href="/netdata/">Netdata</a><a href="/status.json">status.json</a>
  <a href="/docs/copiar.html"><b>Copiar TODOS</b></a>
</nav>
<h1>Troubleshooting</h1>
<h2>systemd en loop (restart storm)</h2>
<pre>sudo systemctl status &lt;slug&gt; -l --no-pager
sudo journalctl -u &lt;slug&gt; -n 200 --no-pager</pre>
<p>Revisar <code>StartLimit*</code>, excepciones en <code>run.py</code> y que se usen módulos (<code>-m</code>) para gunicorn/uvicorn.</p>
<h2>Gunicorn no encontrado</h2>
<p>Usar <code>sys.executable -m gunicorn</code> (respeta venv) o instalar en el venv:</p>
<pre>sudo -u &lt;slug&gt; /srv/projects/&lt;slug&gt;/venv/bin/pip install --upgrade gunicorn</pre>

caddy (HTML importado)

<!doctype html><meta charset="utf-8"><title>Caddy v2</title>
<link rel="stylesheet" href="data:text/css,body{font-family:system-ui;margin:0 auto;max-width:900px;padding:2rem;line-height:1.5}pre,code{background:#f5f5f5;padding:.2em .4em;border-radius:4px}pre{padding:1rem;overflow:auto}">
<nav style="display:flex;gap:.5rem;flex-wrap:wrap;margin:0 0 1rem">
  <a href="/">Estado</a><a href="/docs/">Docs</a><a href="/docs/mapa.html">Mapa</a>
  <a href="/grafana/">Grafana</a><a href="/prometheus/">Prometheus</a>
  <a href="/netdata/">Netdata</a><a href="/status.json">status.json</a>
  <a href="/docs/copiar.html"><b>Copiar TODOS</b></a>
</nav>
<h1>Caddy v2</h1>
<h2>TLS via Cloudflare (DNS-01)</h2>
<pre>{ 
  acme_dns cloudflare {
    api_token {env.CLOUDFLARE_API_TOKEN}
  }
}
(import "includes/*.caddy")</pre>
<h2>Vhost típico</h2>
<pre>sub.dominio.tld {
  import tls_cf
  encode zstd gzip
  @health path /health
  handle_path /health* { reverse_proxy 127.0.0.1:PORT }
  reverse_proxy 127.0.0.1:PORT
}</pre>
<h2>Tips</h2>
<ul>
  <li>Validar: <code>sudo --preserve-env=CLOUDFLARE_API_TOKEN caddy validate --config /etc/caddy/Caddyfile</code></li>
  <li>Logs: <code>journalctl -u caddy -n 100 --no-pager</code></li>
  <li>No duplicar el mismo host en más de un archivo (ambiguous site definition).</li>
</ul>

admin (HTML importado)

<!doctype html><meta charset="utf-8"><title>Admin & API central</title>
<link rel="stylesheet" href="data:text/css,body{font-family:system-ui;margin:0 auto;max-width:900px;padding:2rem;line-height:1.5}pre,code{background:#f5f5f5;padding:.2em .4em;border-radius:4px}pre{padding:1rem;overflow:auto}">
<nav style="display:flex;gap:.5rem;flex-wrap:wrap;margin:0 0 1rem">
  <a href="/">Estado</a><a href="/docs/">Docs</a><a href="/docs/mapa.html">Mapa</a>
  <a href="/grafana/">Grafana</a><a href="/prometheus/">Prometheus</a>
  <a href="/netdata/">Netdata</a><a href="/status.json">status.json</a>
  <a href="/docs/copiar.html"><b>Copiar TODOS</b></a>
</nav>
<h1>Admin & API central</h1>
<ul>
  <li><b>api.illanes00.cl</b>: FastAPI de inventario/health/systemd/DNS/cron/métricas. Rate limit con Redis.</li>
  <li><b>admin.illanes00.cl</b>: Dashboard (consumidor de la API). Protegeremos más adelante.</li>
</ul>
<h2>DB & pooling</h2>
<pre>DATABASE_URL=postgresql://app_user:***@localhost:5432/app_db
REDIS_URL=redis://localhost:6379/0</pre>
<h2>Descubrimiento OpenAPI</h2>
<p>La API central indexa <code>/openapi.json</code> de cada servicio y expone catálogo unificado.</p>

auth (HTML importado)

<!doctype html><meta charset="utf-8"><title>Autenticación</title>
<link rel="stylesheet" href="data:text/css,body{font-family:system-ui;margin:0 auto;max-width:900px;padding:2rem;line-height:1.5}pre,code{background:#f5f5f5;padding:.2em .4em;border-radius:4px}pre{padding:1rem;overflow:auto}">
<nav style="display:flex;gap:.5rem;flex-wrap:wrap;margin:0 0 1rem">
  <a href="/">Estado</a><a href="/docs/">Docs</a><a href="/docs/mapa.html">Mapa</a>
  <a href="/grafana/">Grafana</a><a href="/prometheus/">Prometheus</a>
  <a href="/netdata/">Netdata</a><a href="/status.json">status.json</a>
  <a href="/docs/copiar.html"><b>Copiar TODOS</b></a>
</nav>
<h1>Autenticación</h1>
<p><b>Estado actual:</b> infraestructura lista, no habilitada.</p>
<h2>Opciones</h2>
<ul>
  <li><b>oauth2-proxy</b> por subdominio (Google/GitHub/OIDC).</li>
  <li><b>Authelia</b> como IdP OIDC propio (MFA, grupos, políticas).</li>
  <li><b>Mixto</b>: Authelia (IdP) + oauth2-proxy en cada app.</li>
</ul>
<h2>oauth2-proxy (ejemplo Google)</h2>
<pre>oauth2-proxy \\
  --provider=google \\
  --client-id=... --client-secret=... \\
  --cookie-secret=... \\
  --upstream=http://127.0.0.1:PORT_APP \\
  --redirect-url=https://DOMINIO/oauth2/callback</pre>