Manual operativo
Convenciones & Deploy
- Estructura:
/srv/projects/<slug>/convenv/,.env,run.py,deploy/. - Slugs:
dominio-subdominio. Puertos apps 8101–8199; infra ≥ 9000. - run.py autodetecta:
wsgi:app(gunicorn),main:app(uvicorn/FastAPI) oapp:app(Flask). Fallback expone/health. - systemd:
/etc/systemd/system/<slug>.service,ExecStartusavenv/bin/python run.py. - Caddy: vhosts con snippet
(tls_cf)(DNS-01 Cloudflare) y health. - Monitor:
infra.illanes00.cl+/status.json.
Runbook de deploy rápido
# crear proyecto
sudo mkdir -p /srv/projects/<slug> && cd /srv/projects/<slug>
python3 -m venv venv
echo PORT=<puerto> > .env
venv/bin/pip install --upgrade pip gunicorn uvicorn fastapi flask python-dotenv
# run.py estándar (módulos → respeta venv)
cat > run.py <<'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/<slug>.service >/dev/null <<'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 <slug>
# Caddy vhost (ejemplo)
sudo tee /etc/caddy/sites.d/<slug>.caddy >/dev/null <<'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
Crear un nuevo proyecto
- Elegir slug y puerto libre (8101–8199).
- Crear carpeta + venv +
.env+run.py. - Instalar deps y definir
/health. - Crear unit systemd + vhost Caddy.
- Smoke tests local y público.
sudo mkdir -p /srv/projects/<slug> && cd /srv/projects/<slug>
python3 -m venv venv
echo PORT=<puerto> > .env
venv/bin/pip install fastapi uvicorn gunicorn flask python-dotenv
# (pegar run.py estándar de Deploy)
Backups & Base de datos
- PostgreSQL central (futuro PgBouncer).
- Dumps diarios locales + (opcional) subida a S3.
- Retención sugerida: 7 días locales + 30 en remoto.
Dump manual
PGPASSWORD=*** pg_dump -h localhost -U user dbname | gzip > /var/backups/dbname-$(date +%F).sql.gz
status.json
Documento JSON consumido por el infra-monitor para pintar el estado. Debe incluir cada servicio con systemd y HTTP (local/público) cuando aplique.
Troubleshooting
systemd en loop (restart storm)
sudo systemctl status <slug> -l --no-pager
sudo journalctl -u <slug> -n 200 --no-pager
Revisar StartLimit*, excepciones en run.py y que se usen módulos (-m) para gunicorn/uvicorn.
Gunicorn no encontrado
Usar sys.executable -m gunicorn (respeta venv) o instalar en el venv:
sudo -u <slug> /srv/projects/<slug>/venv/bin/pip install --upgrade gunicorn
Cronograma
Hoy (prioridad máxima)
- Unificar Espacio Público: combinar
ep-seguridad+illanes00-epen una sola plataforma.- Checklist: consolidar modelos/ETL, rutas y frontend; dejar
/seguridadcomo sección. - HTTP: mantener
/health; smoke tests local→público; redirigir host viejo si aplica.
- Checklist: consolidar modelos/ETL, rutas y frontend; dejar
Próximo
- Admin (read-only): panel consumiendo API central; luego proteger.
- API central: catálogo de endpoints (+indexado OpenAPI) y límites con Redis.
- PgBouncer: pool de conexiones; roles por app en PostgreSQL.
- Mapa del sitio: autogenerado desde
/etc/infra/sites.yaml. - Estilo unificado (tokens CSS mínimos).
Observabilidad mínima
- Alertas: 5xx > 1% (5m), service down, p95 > umbral.
- Dashboards: tráfico, latencia, errores, systemd restarts.
Notas de implementación
- Despliegue vía
run.pyautodetect (gunicorn/uvicorn) y unit systemd simple. - Caddy con
(tls_cf),/healthproxyeado y compresión.
Migración & Flip DNS
- Inventariar en
/etc/infra/sites.yaml. - Crear unit systemd + Caddy vhost. Exigir
/health200 local. - TTL bajo (120s). Validar público con
curl -sS --resolve ... - Flip DNS en Cloudflare (A/AAAA a la IP nueva). Smoke tests y métricas.
- TTL normal. Monitorear errores 5xx/latencias en Grafana/Netdata.
Redirects (decomission & aliases)
# ejemplo: ep-seguridad → plataforma unificada
ep-seguridad.illanes00.cl {
import tls_cf
redir https://ep.illanes00.cl/seguridad{uri} permanent
}