Build the Docker image in GitHub Actions, publish to ghcr.io/mauricioabh/arbpulse (latest + sha tags), then SSH into the Hetzner VPS to docker compose pull + up -d with a health-check gate. The VPS no longer builds images, keeping CPU/RAM free for the running apps and making rollbacks a matter of pulling a previous sha tag. - .github/workflows/vps-deploy.yml: build-push (GHCR) + deploy (SSH) jobs - deploy/docker-compose.yml: app image now ghcr.io/mauricioabh/arbpulse - deploy/deploy.sh: pulls from GHCR by default (BUILD=1 for local build), default APP_DIR aligned to /root/projects/arbpulse - docs: deploy/README.md CI/CD section + paths, README deploy section Co-authored-by: Cursor <cursoragent@cursor.com>
5.3 KiB
Deploy en VPS (Hetzner) — Arb Pulse
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.
Deploy automático (CI/CD con GHCR) — flujo normal
Cada push/merge a main dispara .github/workflows/vps-deploy.yml:
- Build en GitHub Actions → imagen publicada en
ghcr.io/mauricioabh/arbpulsecon tagslatest+sha-<commit>(la VPS nunca buildea). - Deploy por SSH → en la VPS:
git reset --hard origin/main(en/root/projects/arbpulse),docker compose pull app,docker compose up -d appy espera del health check.
Secrets del repo: VPS_SSH_KEY (deploy key dedicada, solo para esto),
VPS_HOST, VPS_USER. Rollback: re-ejecutar el workflow desde un commit
anterior (workflow_dispatch) o en la VPS hacer docker compose pull de un
tag sha-<commit> previo.
No editar archivos del repo directamente en la VPS: el deploy hace
git reset --hardy los pisará. La config local vive solo endeploy/.env(no trackeado).
Lo que sigue abajo es el camino manual/bootstrap (primera instalación o fallback). 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 paraarbpulse.wayool.comy punto. - Opción B — Caddy bundleado. Solo para un server fresco sin nada en 80/443.
Prerrequisitos
- VPS Ubuntu/Debian (recomendado 2 vCPU / 2 GB RAM — p. ej. Hetzner CX22).
- Subdominio
arbpulse.wayool.comcon registro A → IP del VPS (ya creado en Cloudflare, ver abajo). - Docker + plugin compose (el
deploy.shlos instala si faltan).
DNS en Cloudflare (zona 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/streamfluye sin buffering del proxy de Cloudflare.
- Motivo: certbot/Caddy emiten el cert vía HTTP-01 (reto directo al origen) y el
SSE de
- Verificar:
dig +short arbpulse.wayool.comdebe devolver la IP del VPS.
Opción A — detrás del nginx existente (recomendado)
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).
# 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 (DOMAIN + secretos opcionales)
nano /root/projects/arbpulse/deploy/.env # DOMAIN=arbpulse.wayool.com ; SENTRY_TRACING=false ; ...
# 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 /root/projects/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
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:
cd /root/projects/arbpulse/deploy
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
docker compose -f /root/projects/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://arbpulse.wayool.com → badge LIVE y los 4 exchanges.
Variables de entorno
Ver .env.vps.example. Claves:
DOMAIN—arbpulse.wayool.com(usado por Caddy en Opción B; informativo en A).SENTRY_DSN— opcional; activa error monitoring.SENTRY_TRACING— dejar enfalse(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
.envreal. Solo.env.vps.examplevive en el repo.
Operación
cd /root/projects/arbpulse/deploy
docker compose logs -f app # logs de la app
docker compose restart app # reiniciar
docker compose down # detener (app; agrega --profile caddy si aplica)
bash /tmp/arbpulse-deploy.sh # actualizar manualmente (git pull main + pull GHCR)
BUILD=1 bash /tmp/arbpulse-deploy.sh # fallback: build local en la VPS
Notas
- 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: noen 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.