diff --git a/deploy/README.md b/deploy/README.md index 8c65f36..beb6dc5 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -1,80 +1,89 @@ # Deploy en VPS (Hetzner) — Arb Pulse -Despliegue con Docker Compose: el contenedor `app` (Node + tsx) detrás de -`caddy`, que emite HTTPS automáticamente (Let's Encrypt). Pensado para correr -24/7 desde la rama `main`. +Despliegue con Docker Compose para correr 24/7 desde la rama `main`. El contenedor +`app` (Node + tsx) queda en `127.0.0.1:8080` y un **reverse proxy del host** expone +HTTPS. Dos modos: + +- **Opción A — detrás de nginx existente (recomendado en este VPS).** El servidor ya + corre nginx en 80/443 con certbot para otras apps (p. ej. `consumet.wayool.com`, + `openclaw.wayool.com`). Se agrega un vhost para `arbpulse.wayool.com` y punto. +- **Opción B — Caddy bundleado.** Solo para un server **fresco** sin nada en 80/443. ## Prerrequisitos 1. VPS Ubuntu/Debian (recomendado 2 vCPU / 2 GB RAM — p. ej. Hetzner CX22). -2. Subdominio en Cloudflare (zona `wayool.com`): `arbpulse.wayool.com` con un - registro **A → IP del VPS**. Ver "DNS en Cloudflare" abajo. -3. Puertos abiertos: - ```bash - sudo ufw allow 22 && sudo ufw allow 80 && sudo ufw allow 443 && sudo ufw --force enable - ``` +2. Subdominio `arbpulse.wayool.com` con registro **A → IP del VPS** (ya creado en + Cloudflare, ver abajo). +3. Docker + plugin compose (el `deploy.sh` los instala si faltan). ## DNS en Cloudflare (zona wayool.com) -El dominio `wayool.com` está gestionado por Cloudflare. La app se sirve en el -subdominio **`arbpulse.wayool.com`**. +- Registro **A**, nombre `arbpulse`, contenido = IP del VPS, **"DNS only" (nube gris)**. + - Motivo: certbot/Caddy emiten el cert vía HTTP-01 (reto directo al origen) y el + SSE de `/api/stream` fluye sin buffering del proxy de Cloudflare. +- Verificar: `dig +short arbpulse.wayool.com` debe devolver la IP del VPS. -- Registro: **A**, nombre `arbpulse`, contenido = IP pública del VPS. -- **Proxy: "DNS only" (nube gris), NO proxied (nube naranja).** - - Motivo: Caddy emite el cert Let's Encrypt vía HTTP-01 (requiere que el reto en - el puerto 80 llegue directo al origen), y el stream SSE de `/api/stream` fluye - sin el buffering/timeout del proxy de Cloudflare. - - Si más adelante querés proxear con Cloudflare (naranja) para ocultar la IP, - hay que cambiar Caddy a challenge DNS-01 (plugin `caddy-dns/cloudflare` + token - de API) y poner SSL/TLS en "Full (strict)". No es necesario para arrancar. -- Verificar propagación: `dig +short arbpulse.wayool.com` debe devolver la IP del - VPS antes de que Caddy pida el certificado. +--- -## Deploy (para el agente del VPS) +## Opción A — detrás del nginx existente (recomendado) -Todo lo hace el script idempotente `deploy.sh` (instala Docker si falta, clona o -actualiza el repo, y levanta los contenedores). Es seguro re-ejecutarlo. +No toca tus otras apps ni Caddy. Solo levanta el contenedor `app` en loopback y le +pone un vhost de nginx con certbot (mismo patrón que consumet/openclaw). ```bash -# 1) Traer el script y ejecutarlo (primera vez: crea .env y se detiene) +# 1) Traer y correr el script (1ra vez: crea .env y se detiene). App-only por default. curl -fsSL https://raw.githubusercontent.com/mauricioabh/arbpulse/main/deploy/deploy.sh -o /tmp/arbpulse-deploy.sh bash /tmp/arbpulse-deploy.sh -# 2) Editar el .env con el dominio y secretos -nano /opt/arbpulse/deploy/.env # DOMAIN=... ; SENTRY_DSN=... (opcional) ; UPSTASH_* (opcional) +# 2) Editar el .env (DOMAIN + secretos opcionales) +nano /opt/arbpulse/deploy/.env # DOMAIN=arbpulse.wayool.com ; SENTRY_TRACING=false ; ... -# 3) Volver a ejecutar para construir y arrancar +# 3) Volver a correr: build + up del contenedor app (127.0.0.1:8080) bash /tmp/arbpulse-deploy.sh + +# 4) Instalar el vhost de nginx y emitir el cert +sudo cp /opt/arbpulse/deploy/nginx/arbpulse.wayool.com.conf /etc/nginx/sites-available/arbpulse.wayool.com +sudo ln -s /etc/nginx/sites-available/arbpulse.wayool.com /etc/nginx/sites-enabled/ +sudo nginx -t && sudo systemctl reload nginx +sudo certbot --nginx -d arbpulse.wayool.com ``` -Alternativa (si ya está clonado el repo en `/opt/arbpulse`): +El vhost (`deploy/nginx/arbpulse.wayool.com.conf`) trae `proxy_buffering off` y +timeouts largos para que el SSE funcione. certbot copia el bloque `location` al +server TLS que genera, así que los ajustes SSE se mantienen en HTTPS. + +## Opción B — Caddy bundleado (server fresco, sin nginx) + +Solo si nada más usa 80/443: ```bash cd /opt/arbpulse/deploy -cp .env.vps.example .env && nano .env # solo la primera vez -docker compose up -d --build +cp .env.vps.example .env && nano .env # DOMAIN=arbpulse.wayool.com ... +WITH_CADDY=1 bash /tmp/arbpulse-deploy.sh # o: docker compose --profile caddy up -d --build ``` +--- + ## Verificar ```bash -docker compose -f /opt/arbpulse/deploy/docker-compose.yml ps -curl -s http://127.0.0.1:8080/api/health # local -curl -s https://TU_DOMINIO/api/health # público (tras propagar DNS) +docker compose -f /opt/arbpulse/deploy/docker-compose.yml ps # app healthy/running +curl -s http://127.0.0.1:8080/api/health # local +curl -s https://arbpulse.wayool.com/api/health # público (HTTPS) ``` -Dashboard: `https://TU_DOMINIO` → badge **LIVE** y los 4 exchanges en la matriz. +Dashboard: `https://arbpulse.wayool.com` → badge **LIVE** y los 4 exchanges. ## Variables de entorno Ver `.env.vps.example`. Claves: -- `DOMAIN` — dominio para Caddy/HTTPS (obligatorio). +- `DOMAIN` — `arbpulse.wayool.com` (usado por Caddy en Opción B; informativo en A). - `SENTRY_DSN` — opcional; activa error monitoring. -- `SENTRY_TRACING` — dejar en `false` (default) para no consumir la cuota - free-tier de spans en operación 24/7. -- `UPSTASH_REDIS_REST_URL` / `UPSTASH_REDIS_REST_TOKEN` — opcionales (rate limit - + cache). Vacío = desactivado. +- `SENTRY_TRACING` — dejar en `false` (default) para no consumir la cuota free-tier + de spans en operación 24/7. +- `UPSTASH_REDIS_REST_URL` / `UPSTASH_REDIS_REST_TOKEN` — opcionales (rate limit + + cache). Vacío = desactivado. > Nunca comitees el `.env` real. Solo `.env.vps.example` vive en el repo. @@ -84,15 +93,15 @@ Ver `.env.vps.example`. Claves: cd /opt/arbpulse/deploy docker compose logs -f app # logs de la app docker compose restart app # reiniciar -docker compose down # detener +docker compose down # detener (app; agrega --profile caddy si aplica) bash /tmp/arbpulse-deploy.sh # actualizar (git pull main + rebuild) ``` ## Notas -- Caddy usa `flush_interval -1` para que el SSE (`/api/stream`) fluya en tiempo - real; no comprimir `text/event-stream`. -- El puerto 8080 se publica solo en `127.0.0.1` (no expuesto a Internet); el - tráfico público entra por Caddy (80/443). -- Región: Hetzner no tiene Asia; para menor latencia a Binance/Bybit/OKX evalúa - un VPS en Singapur. Para Kraken (EU) Falkenstein/Helsinki va bien. +- El puerto 8080 se publica solo en `127.0.0.1` (no expuesto a Internet); el tráfico + público entra por el reverse proxy del host (nginx o Caddy). +- La app ya envía `X-Accel-Buffering: no` en el SSE, que nginx respeta para no + bufferear ese response. +- Región: Hetzner no tiene Asia; para menor latencia a Binance/Bybit/OKX evalúa un + VPS en Singapur. Para Kraken (EU) Falkenstein/Helsinki va bien. diff --git a/deploy/deploy.sh b/deploy/deploy.sh index c5bfc78..cb75288 100755 --- a/deploy/deploy.sh +++ b/deploy/deploy.sh @@ -3,11 +3,16 @@ # Safe to re-run: installs Docker if missing, clones or fast-forwards the repo, # then (re)builds and restarts the containers. On first run it creates .env from # the example and stops so you can fill in DOMAIN + secrets. +# +# Default: runs ONLY the app on 127.0.0.1:8080 (put it behind your host reverse +# proxy — see deploy/nginx/). Set WITH_CADDY=1 to also start the bundled Caddy on +# 80/443 (only for a fresh server with nothing else on those ports). set -euo pipefail REPO_URL="${REPO_URL:-https://github.com/mauricioabh/arbpulse.git}" APP_DIR="${APP_DIR:-/opt/arbpulse}" BRANCH="${BRANCH:-main}" +WITH_CADDY="${WITH_CADDY:-0}" echo "==> Arb Pulse VPS deploy (branch: $BRANCH, dir: $APP_DIR)" @@ -48,8 +53,13 @@ if [ ! -f .env ]; then fi # 4) Build + run -echo "==> Building and starting containers..." -docker compose up -d --build +if [ "$WITH_CADDY" = "1" ]; then + echo "==> Building and starting app + bundled Caddy (ports 80/443)..." + docker compose --profile caddy up -d --build +else + echo "==> Building and starting app only (127.0.0.1:8080)..." + docker compose up -d --build +fi echo "==> Waiting for health..." sleep 8 @@ -60,5 +70,11 @@ if curl -fsS http://127.0.0.1:8080/api/health >/dev/null 2>&1; then else echo "!! Health check not passing yet. Inspect: docker compose logs -f app" fi -echo "==> If your domain's A record points here, Caddy will issue HTTPS automatically." -echo "==> Verify: curl -s https://\$DOMAIN/api/health" + +if [ "$WITH_CADDY" = "1" ]; then + echo "==> Bundled Caddy will issue HTTPS once DNS for \$DOMAIN points here." + echo "==> Verify: curl -s https://\$DOMAIN/api/health" +else + echo "==> App is bound to 127.0.0.1:8080. Put it behind your host reverse proxy:" + echo " see $APP_DIR/deploy/nginx/arbpulse.wayool.com.conf (nginx + certbot)." +fi diff --git a/deploy/docker-compose.yml b/deploy/docker-compose.yml index bd50f48..b4cfeb4 100644 --- a/deploy/docker-compose.yml +++ b/deploy/docker-compose.yml @@ -1,5 +1,13 @@ -# Arb Pulse — VPS deployment (app + Caddy reverse proxy with automatic HTTPS). -# Usage: from this directory, `docker compose up -d --build`. +# Arb Pulse — VPS deployment. +# +# Default (`docker compose up -d --build`): runs ONLY the app container, bound to +# 127.0.0.1:8080. Put it behind your existing reverse proxy (nginx/Caddy/Traefik) +# on the host — see deploy/nginx/arbpulse.wayool.com.conf. This is the right mode +# when the host already owns ports 80/443 for other apps. +# +# Optional bundled Caddy (`docker compose --profile caddy up -d --build`): only for +# a FRESH server with nothing else on 80/443; Caddy then handles automatic HTTPS. +# # Requires a ./.env file (copy from .env.vps.example and fill DOMAIN + secrets). services: @@ -12,7 +20,7 @@ services: restart: unless-stopped env_file: - ./.env - # Bind to loopback only; public traffic goes through Caddy on 80/443. + # Bind to loopback only; the host reverse proxy forwards public traffic here. ports: - "127.0.0.1:8080:8080" healthcheck: @@ -22,10 +30,13 @@ services: retries: 3 start_period: 15s + # Opt-in only via `--profile caddy`. Do NOT enable if the host already runs + # nginx/another proxy on 80/443 — the ports would collide. caddy: image: caddy:2-alpine container_name: arbpulse-caddy restart: unless-stopped + profiles: ["caddy"] depends_on: - app ports: diff --git a/deploy/nginx/arbpulse.wayool.com.conf b/deploy/nginx/arbpulse.wayool.com.conf new file mode 100644 index 0000000..7655d2f --- /dev/null +++ b/deploy/nginx/arbpulse.wayool.com.conf @@ -0,0 +1,35 @@ +# Arb Pulse — nginx vhost (host-level), to sit alongside consumet/openclaw. +# +# Install: +# sudo cp deploy/nginx/arbpulse.wayool.com.conf /etc/nginx/sites-available/arbpulse.wayool.com +# sudo ln -s /etc/nginx/sites-available/arbpulse.wayool.com /etc/nginx/sites-enabled/ +# sudo nginx -t && sudo systemctl reload nginx +# sudo certbot --nginx -d arbpulse.wayool.com # issues cert + adds the 443 block + HTTP->HTTPS redirect +# +# certbot copies the location block into the TLS server it generates, so the +# SSE-friendly proxy settings below carry over to HTTPS. + +server { + listen 80; + listen [::]:80; + server_name arbpulse.wayool.com; + + # Proxy everything to the app container (loopback-published by docker compose). + location / { + proxy_pass http://127.0.0.1:8080; + proxy_http_version 1.1; + + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # SSE (/api/stream): stream in real time, never buffer, keep long-lived. + proxy_buffering off; + proxy_cache off; + proxy_set_header Connection ""; + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; + chunked_transfer_encoding off; + } +}