mirror of
https://github.com/mauricioabh/arbpulse.git
synced 2026-07-27 15:47:43 +00:00
feat(deploy): automatic VPS deploy via GHCR on push to main
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>
This commit is contained in:
@@ -0,0 +1,92 @@
|
|||||||
|
# Production deploy: build the image in CI, publish to GHCR, then SSH into the
|
||||||
|
# Hetzner VPS to pull + restart. The VPS never builds — it only pulls the image
|
||||||
|
# that passed CI. Rollback = re-run this workflow from an older commit, or
|
||||||
|
# `docker compose pull` a previous sha-tag on the VPS.
|
||||||
|
#
|
||||||
|
# Secrets: VPS_SSH_KEY (dedicated deploy key), VPS_HOST, VPS_USER.
|
||||||
|
|
||||||
|
name: VPS Deploy
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: vps-deploy
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
env:
|
||||||
|
IMAGE: ghcr.io/${{ github.repository }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-push:
|
||||||
|
name: Build & push to GHCR
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- uses: docker/metadata-action@v5
|
||||||
|
id: meta
|
||||||
|
with:
|
||||||
|
images: ${{ env.IMAGE }}
|
||||||
|
tags: |
|
||||||
|
type=raw,value=latest
|
||||||
|
type=sha,format=long
|
||||||
|
|
||||||
|
- uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
push: true
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
cache-from: type=gha
|
||||||
|
cache-to: type=gha,mode=max
|
||||||
|
|
||||||
|
deploy:
|
||||||
|
name: Deploy to VPS
|
||||||
|
needs: build-push
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Setup SSH
|
||||||
|
run: |
|
||||||
|
mkdir -p ~/.ssh
|
||||||
|
printf '%s' "${{ secrets.VPS_SSH_KEY }}" > ~/.ssh/deploy_key
|
||||||
|
chmod 600 ~/.ssh/deploy_key
|
||||||
|
ssh-keyscan -H "${{ secrets.VPS_HOST }}" >> ~/.ssh/known_hosts 2>/dev/null
|
||||||
|
|
||||||
|
- name: Pull image and restart app
|
||||||
|
run: |
|
||||||
|
ssh -i ~/.ssh/deploy_key "${{ secrets.VPS_USER }}@${{ secrets.VPS_HOST }}" bash -s <<'EOF'
|
||||||
|
set -euo pipefail
|
||||||
|
cd /root/projects/arbpulse
|
||||||
|
git fetch origin main
|
||||||
|
git checkout main
|
||||||
|
git reset --hard origin/main
|
||||||
|
cd deploy
|
||||||
|
docker compose pull app
|
||||||
|
docker compose up -d app
|
||||||
|
echo "waiting for health..."
|
||||||
|
for i in $(seq 1 12); do
|
||||||
|
if curl -fsS http://127.0.0.1:8080/api/health >/dev/null 2>&1; then
|
||||||
|
echo "app healthy"
|
||||||
|
docker image prune -f >/dev/null
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
sleep 5
|
||||||
|
done
|
||||||
|
echo "app did not become healthy in time" >&2
|
||||||
|
docker compose logs --tail 50 app >&2
|
||||||
|
exit 1
|
||||||
|
EOF
|
||||||
@@ -41,7 +41,7 @@ Capturas completas (scroll): [`dashboard-live.png`](docs/screenshots/dashboard-l
|
|||||||
| Feeds | WebSocket nativo (`ws`) — APIs públicas |
|
| Feeds | WebSocket nativo (`ws`) — APIs públicas |
|
||||||
| Frontend | React 18, Vite, Tailwind CSS v3 |
|
| Frontend | React 18, Vite, Tailwind CSS v3 |
|
||||||
| Estado | In-memory (sin base de datos) |
|
| Estado | In-memory (sin base de datos) |
|
||||||
| Deploy | [Fly.io](https://fly.io) — Docker multi-stage (`Dockerfile` + `fly.toml`) |
|
| Deploy | VPS Hetzner — Docker Compose + GHCR (CI/CD en `vps-deploy.yml`); Fly.io como alternativa manual |
|
||||||
|
|
||||||
No se requieren API keys: los feeds de mercado son públicos.
|
No se requieren API keys: los feeds de mercado son públicos.
|
||||||
|
|
||||||
@@ -308,21 +308,19 @@ Contrato generado desde Zod (`src/interfaces/http/schemas/`) con `@asteasolution
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Deploy en Fly.io
|
## Deploy (producción: VPS Hetzner + GHCR)
|
||||||
|
|
||||||
El repo incluye `Dockerfile` (multi-stage: build Vite + runtime Node), `fly.toml` y el workflow `.github/workflows/fly-deploy.yml`.
|
Producción corre en una **VPS de Hetzner** (`https://arbpulse.wayool.com`) con Docker Compose detrás de nginx. El deploy es **automático**:
|
||||||
|
|
||||||
1. Instala [flyctl](https://fly.io/docs/flyctl/install/) e inicia sesión: `fly auth login`.
|
1. Push/merge a `main` dispara `.github/workflows/vps-deploy.yml`.
|
||||||
2. Desde la raíz del repo: `fly launch` (primera vez) o `fly deploy`.
|
2. GitHub Actions buildea la imagen (`Dockerfile` multi-stage) y la publica en **GHCR** (`ghcr.io/mauricioabh/arbpulse`, tags `latest` + `sha-<commit>`).
|
||||||
3. **Región primaria:** `sin` (Singapore) — menor latencia a matching engines de Binance, Bybit y OKX.
|
3. El workflow entra por SSH a la VPS, hace `docker compose pull` + `up -d` y espera el health check (`GET /api/health`).
|
||||||
4. Fly expone el servicio en el puerto interno **8080** con healthcheck `GET /api/health`.
|
|
||||||
5. Variables de entorno: opcionales; `fly.toml` incluye defaults razonables (`STALE_MS`, inventario inicial, etc.).
|
|
||||||
6. **CI/CD:** cada push a `main` dispara deploy automático vía GitHub Actions (`fly-deploy.yml`; requiere secret `FLY_API_TOKEN`). Los PRs pasan `CI / quality` (typecheck, test, build) sin desplegar.
|
|
||||||
7. Abre la URL pública: badge **LIVE**, cuatro venues en la price matrix, y —en mercado real— sobre todo oportunidades rechazadas por fees (esperado).
|
|
||||||
|
|
||||||
> Producción usa **Fly.io**. `railway.json` es un artefacto legacy del deploy anterior; no lo uses.
|
Detalles, bootstrap manual y rollback: [`deploy/README.md`](deploy/README.md).
|
||||||
|
|
||||||
Para una demo con actividad visible en pocos segundos, define `DEMO_MODE=true` en Fly (`fly secrets set` / `[env]` en `fly.toml`) o actívalo desde Controls.
|
> Fly.io queda como opción alternativa manual (`fly-deploy.yml` vía workflow_dispatch, requiere `FLY_API_TOKEN`; región recomendada `sin`). `railway.json` es un artefacto legacy; no lo uses.
|
||||||
|
|
||||||
|
Para una demo con actividad visible en pocos segundos, define `DEMO_MODE=true` en `deploy/.env` de la VPS o actívalo desde Controls.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
+30
-7
@@ -2,7 +2,29 @@
|
|||||||
|
|
||||||
Despliegue con Docker Compose para correr 24/7 desde la rama `main`. El contenedor
|
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
|
`app` (Node + tsx) queda en `127.0.0.1:8080` y un **reverse proxy del host** expone
|
||||||
HTTPS. Dos modos:
|
HTTPS.
|
||||||
|
|
||||||
|
## Deploy automático (CI/CD con GHCR) — flujo normal
|
||||||
|
|
||||||
|
Cada push/merge a `main` dispara `.github/workflows/vps-deploy.yml`:
|
||||||
|
|
||||||
|
1. **Build en GitHub Actions** → imagen publicada en `ghcr.io/mauricioabh/arbpulse`
|
||||||
|
con tags `latest` + `sha-<commit>` (la VPS nunca buildea).
|
||||||
|
2. **Deploy por SSH** → en la VPS: `git reset --hard origin/main` (en
|
||||||
|
`/root/projects/arbpulse`), `docker compose pull app`, `docker compose up -d app`
|
||||||
|
y 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 --hard` y los pisará. La config local vive solo en `deploy/.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
|
- **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`,
|
corre nginx en 80/443 con certbot para otras apps (p. ej. `consumet.wayool.com`,
|
||||||
@@ -36,13 +58,13 @@ curl -fsSL https://raw.githubusercontent.com/mauricioabh/arbpulse/main/deploy/de
|
|||||||
bash /tmp/arbpulse-deploy.sh
|
bash /tmp/arbpulse-deploy.sh
|
||||||
|
|
||||||
# 2) Editar el .env (DOMAIN + secretos opcionales)
|
# 2) Editar el .env (DOMAIN + secretos opcionales)
|
||||||
nano /opt/arbpulse/deploy/.env # DOMAIN=arbpulse.wayool.com ; SENTRY_TRACING=false ; ...
|
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)
|
# 3) Volver a correr: build + up del contenedor app (127.0.0.1:8080)
|
||||||
bash /tmp/arbpulse-deploy.sh
|
bash /tmp/arbpulse-deploy.sh
|
||||||
|
|
||||||
# 4) Instalar el vhost de nginx y emitir el cert
|
# 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 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 ln -s /etc/nginx/sites-available/arbpulse.wayool.com /etc/nginx/sites-enabled/
|
||||||
sudo nginx -t && sudo systemctl reload nginx
|
sudo nginx -t && sudo systemctl reload nginx
|
||||||
sudo certbot --nginx -d arbpulse.wayool.com
|
sudo certbot --nginx -d arbpulse.wayool.com
|
||||||
@@ -57,7 +79,7 @@ server TLS que genera, así que los ajustes SSE se mantienen en HTTPS.
|
|||||||
Solo si nada más usa 80/443:
|
Solo si nada más usa 80/443:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd /opt/arbpulse/deploy
|
cd /root/projects/arbpulse/deploy
|
||||||
cp .env.vps.example .env && nano .env # DOMAIN=arbpulse.wayool.com ...
|
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
|
WITH_CADDY=1 bash /tmp/arbpulse-deploy.sh # o: docker compose --profile caddy up -d --build
|
||||||
```
|
```
|
||||||
@@ -67,7 +89,7 @@ WITH_CADDY=1 bash /tmp/arbpulse-deploy.sh # o: docker compose --profile ca
|
|||||||
## Verificar
|
## Verificar
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose -f /opt/arbpulse/deploy/docker-compose.yml ps # app healthy/running
|
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 http://127.0.0.1:8080/api/health # local
|
||||||
curl -s https://arbpulse.wayool.com/api/health # público (HTTPS)
|
curl -s https://arbpulse.wayool.com/api/health # público (HTTPS)
|
||||||
```
|
```
|
||||||
@@ -90,11 +112,12 @@ Ver `.env.vps.example`. Claves:
|
|||||||
## Operación
|
## Operación
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd /opt/arbpulse/deploy
|
cd /root/projects/arbpulse/deploy
|
||||||
docker compose logs -f app # logs de la app
|
docker compose logs -f app # logs de la app
|
||||||
docker compose restart app # reiniciar
|
docker compose restart app # reiniciar
|
||||||
docker compose down # detener (app; agrega --profile caddy si aplica)
|
docker compose down # detener (app; agrega --profile caddy si aplica)
|
||||||
bash /tmp/arbpulse-deploy.sh # actualizar (git pull main + rebuild)
|
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
|
## Notas
|
||||||
|
|||||||
+25
-9
@@ -1,8 +1,13 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Idempotent VPS deploy for Arb Pulse (Ubuntu/Debian).
|
# Idempotent VPS deploy for Arb Pulse (Ubuntu/Debian).
|
||||||
# Safe to re-run: installs Docker if missing, clones or fast-forwards the repo,
|
# 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
|
# pulls the CI-built image from GHCR (or builds locally with BUILD=1) and
|
||||||
# the example and stops so you can fill in DOMAIN + secrets.
|
# restarts the containers. On first run it creates .env from the example and
|
||||||
|
# stops so you can fill in DOMAIN + secrets.
|
||||||
|
#
|
||||||
|
# NOTE: normal production deploys are automatic — push/merge to `main` triggers
|
||||||
|
# .github/workflows/vps-deploy.yml (build in CI -> GHCR -> SSH pull + restart).
|
||||||
|
# This script is the manual/bootstrap path.
|
||||||
#
|
#
|
||||||
# Default: runs ONLY the app on 127.0.0.1:8080 (put it behind your host reverse
|
# 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
|
# proxy — see deploy/nginx/). Set WITH_CADDY=1 to also start the bundled Caddy on
|
||||||
@@ -10,9 +15,11 @@
|
|||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
REPO_URL="${REPO_URL:-https://github.com/mauricioabh/arbpulse.git}"
|
REPO_URL="${REPO_URL:-https://github.com/mauricioabh/arbpulse.git}"
|
||||||
APP_DIR="${APP_DIR:-/opt/arbpulse}"
|
APP_DIR="${APP_DIR:-/root/projects/arbpulse}"
|
||||||
BRANCH="${BRANCH:-main}"
|
BRANCH="${BRANCH:-main}"
|
||||||
WITH_CADDY="${WITH_CADDY:-0}"
|
WITH_CADDY="${WITH_CADDY:-0}"
|
||||||
|
# BUILD=1 builds the image locally instead of pulling from GHCR (fallback).
|
||||||
|
BUILD="${BUILD:-0}"
|
||||||
|
|
||||||
echo "==> Arb Pulse VPS deploy (branch: $BRANCH, dir: $APP_DIR)"
|
echo "==> Arb Pulse VPS deploy (branch: $BRANCH, dir: $APP_DIR)"
|
||||||
|
|
||||||
@@ -52,13 +59,22 @@ if [ ! -f .env ]; then
|
|||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 4) Build + run
|
# 4) Image: pull from GHCR (default) or build locally (BUILD=1)
|
||||||
if [ "$WITH_CADDY" = "1" ]; then
|
if [ "$BUILD" = "1" ]; then
|
||||||
echo "==> Building and starting app + bundled Caddy (ports 80/443)..."
|
echo "==> Building image locally..."
|
||||||
docker compose --profile caddy up -d --build
|
docker compose build app
|
||||||
else
|
else
|
||||||
echo "==> Building and starting app only (127.0.0.1:8080)..."
|
echo "==> Pulling image from GHCR..."
|
||||||
docker compose up -d --build
|
docker compose pull app
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 5) Run
|
||||||
|
if [ "$WITH_CADDY" = "1" ]; then
|
||||||
|
echo "==> Starting app + bundled Caddy (ports 80/443)..."
|
||||||
|
docker compose --profile caddy up -d
|
||||||
|
else
|
||||||
|
echo "==> Starting app only (127.0.0.1:8080)..."
|
||||||
|
docker compose up -d app
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "==> Waiting for health..."
|
echo "==> Waiting for health..."
|
||||||
|
|||||||
@@ -12,10 +12,13 @@
|
|||||||
|
|
||||||
services:
|
services:
|
||||||
app:
|
app:
|
||||||
|
# Normal flow: the image is built by CI (.github/workflows/vps-deploy.yml)
|
||||||
|
# and pulled from GHCR. `build:` remains for local/manual builds only
|
||||||
|
# (docker compose build app).
|
||||||
build:
|
build:
|
||||||
context: ..
|
context: ..
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
image: arbpulse:latest
|
image: ghcr.io/mauricioabh/arbpulse:latest
|
||||||
container_name: arbpulse
|
container_name: arbpulse
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
env_file:
|
env_file:
|
||||||
|
|||||||
Reference in New Issue
Block a user