Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 28 additions & 43 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -1,58 +1,43 @@
name: Deploy to EC2

# Auto-deploy on every push to main (or run manually). Keyless: assumes an AWS
# IAM role via GitHub OIDC and triggers the deploy on the instance through SSM
# (no SSH, no AWS keys stored in GitHub).
name: Deploy API (srv07)

# Autodeploy de la API en srv07 al hacer push a main.
#
# La clave `SRV07_DEPLOY_KEY` entra como root pero está atada a un *forced
# command* en /root/.ssh/authorized_keys: sshd ejecuta siempre
# /usr/local/sbin/responsegrid-deploy e ignora lo que mande el cliente. Es decir,
# esta clave no puede hacer nada más que desplegar, y el script de deploy vive en
# el servidor (con git reset --hard, build, migraciones y health check).
# El `script:` de abajo existe solo porque la action lo exige.
on:
push:
branches: [main]
paths:
- 'apps/api/**'
- 'packages/**'
- 'deploy/**'
- 'package.json'
- 'pnpm-workspace.yaml'
- 'pnpm-lock.yaml'
- '.github/workflows/deploy.yml'
workflow_dispatch:

concurrency:
group: responsegrid-deploy
group: deploy-api-srv07
cancel-in-progress: false

permissions:
id-token: write
contents: read
# El job solo usa secretos: sin permisos para GITHUB_TOKEN.
permissions: {}

jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Configure AWS credentials (OIDC)
uses: aws-actions/configure-aws-credentials@254c19bd240aabef8777f48595e9d2d7b972184b # v6.2.1
- name: SSH deploy to srv07
uses: appleboy/ssh-action@7eaf76671a0d7eec5d98ee897acda4f968735a17 # v1.2.0
with:
role-to-assume: arn:aws:iam::386707340306:role/responsegrid-gha-deploy
aws-region: us-east-1

- name: Deploy on EC2 via SSM
run: |
set -euo pipefail
CMD='cd /opt/responsegrid && git fetch origin && git reset --hard origin/main && printf "DD_VERSION=%s\n" "$(git rev-parse --short HEAD)" > deploy/.env.version && install -m 644 deploy/responsegrid.service /etc/systemd/system/responsegrid.service && systemctl daemon-reload && systemctl enable responsegrid.service && docker compose -f deploy/docker-compose.prod.yml up -d --build && docker compose -f deploy/docker-compose.prod.yml ps'
PARAMS=$(jq -n --arg c "$CMD" '{commands: [$c]}')
CID=$(aws ssm send-command \
--instance-ids i-0f5979080ad5da6e5 \
--document-name AWS-RunShellScript \
--comment "GHA ${GITHUB_SHA::7}" \
--timeout-seconds 2400 \
--parameters "$PARAMS" \
--query Command.CommandId --output text)
echo "SSM CommandId: $CID"
for i in $(seq 1 220); do
sleep 9
ST=$(aws ssm get-command-invocation --command-id "$CID" --instance-id i-0f5979080ad5da6e5 --query Status --output text 2>/dev/null || echo Pending)
echo "status: $ST"
if [ "$ST" = "Success" ]; then
aws ssm get-command-invocation --command-id "$CID" --instance-id i-0f5979080ad5da6e5 --query StandardOutputContent --output text | tail -25
exit 0
fi
case "$ST" in
Failed|Cancelled|TimedOut)
echo "::error::deploy $ST"
aws ssm get-command-invocation --command-id "$CID" --instance-id i-0f5979080ad5da6e5 --query StandardErrorContent --output text | tail -40
exit 1 ;;
esac
done
echo "::error::timed out waiting for SSM deploy"; exit 1
host: ${{ secrets.SRV07_HOST }}
username: ${{ secrets.SRV07_USER }}
key: ${{ secrets.SRV07_DEPLOY_KEY }}
command_timeout: 20m
script: |
true
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Canonical instructions for any AI agent or contributor working in this repo. Rea

## What ResponseGrid is

Multi-emergency **material aid coordination + logistics** platform (org: **Global Emergency**). Live: web `https://responsegrid.app` (Vercel), API `https://api.responsegrid.app` (EC2). Activated per emergency; data isolated by `emergency_id`/slug. Connects citizens, organizations and coordinators during a disaster.
Multi-emergency **material aid coordination + logistics** platform (org: **Global Emergency**). Live: web `https://responsegrid.app` (Vercel), API `https://responsegrid-api.globalemergency.online` (srv07, Plesk + Docker; the legacy `https://api.responsegrid.app` is a Cloudflare 308 redirect to it). Activated per emergency; data isolated by `emergency_id`/slug. Connects citizens, organizations and coordinators during a disaster.

**In scope:** collection/logistic points (puntos de acopio) **with declared material inventory per place**, validated needs (with 48h freshness), material offers + matching to needs, **a single shared catalogue of supplies + categories** (insumos), **transport capacity + shipments** (logistics), volunteers + tasks, field reports (incident/stock/status), real-time Leaflet map, **authorization** (roles/grants/groups/API keys), public read-only API + developer `/docs`.

Expand Down Expand Up @@ -92,7 +92,7 @@ Verify `dynamic(ssr:false)` components (the Leaflet map) only in a **production*

## Deploy

A merged PR → push to `main` triggers: **GitHub Action** deploys the API to EC2 via SSM (builds the Docker image, applies migrations with `migrate.sh`) **and** Vercel auto-deploys the web. The CI gate having passed is what keeps prod healthy.
A merged PR → push to `main` triggers: **GitHub Action** deploys the API to srv07 over SSH (builds the Docker image, applies migrations with `migrate.sh`; see `docs/deploy/srv07.md`) **and** Vercel auto-deploys the web. The CI gate having passed is what keeps prod healthy.

## Public emergency for testing

Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,8 @@ Flujo de trabajo:

## 🚀 Despliegue

- [`docs/deploy/aws-free-tier.md`](docs/deploy/aws-free-tier.md) — guía paso a paso: **web en Vercel + API/Postgres/Redis en una EC2 + S3** (la guía usa t3.micro free-tier; producción corre en **t3.small** por el agente de observabilidad). Artefactos en [`deploy/`](deploy) (Dockerfile, `docker-compose.prod.yml`, migraciones, Caddy).
- [`docs/deploy/srv07.md`](docs/deploy/srv07.md) — **despliegue actual de producción**: web en Vercel + API/Postgres/Redis en srv07 (Plesk + Docker) en `responsegrid-api.globalemergency.online`, detrás de nginx (`api.responsegrid.app` redirige ahí). Artefactos en [`deploy/`](deploy) (`docker-compose.srv07.yml`, unit de systemd, migraciones).
- [`docs/deploy/aws-free-tier.md`](docs/deploy/aws-free-tier.md) — alternativa en AWS, guía paso a paso: **web en Vercel + API/Postgres/Redis en una EC2 + S3** (la guía usa t3.micro free-tier; producción corre en **t3.small** por el agente de observabilidad). Artefactos en [`deploy/`](deploy) (Dockerfile, `docker-compose.prod.yml`, migraciones, Caddy).
- [`deploy/datadog.md`](deploy/datadog.md) — observabilidad: agente **Datadog** (host, contenedores, Postgres+DBM, Redis, logs y **APM/trazas**), sitio EU.

## 📚 Documentación
Expand Down
98 changes: 98 additions & 0 deletions deploy/docker-compose.srv07.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# ResponseGrid — stack de producción para srv07 (Plesk + Docker compartido).
#
# Diferencias con docker-compose.prod.yml (EC2):
# - Sin Caddy: Plesk/nginx ya ocupa 80/443 y termina TLS para
# responsegrid-api.globalemergency.online, y hace proxy_pass a 127.0.0.1:3100.
# - La API publica un puerto SOLO en loopback (127.0.0.1:3100 -> 3000).
# - Sin agente Datadog propio: srv07 ya tiene uno. Las trazas APM se envían
# al agente del host por la gateway de la red bridge (DD_AGENT_HOST).
# - Sin rol de instancia IAM: las credenciales de S3 son de un usuario IAM
# dedicado y viven en deploy/.env.
#
# Uso (en srv07, como root):
# cd /opt/responsegrid
# docker compose -p responsegrid -f deploy/docker-compose.srv07.yml up -d --build

name: responsegrid

services:
postgres:
image: postgres:16
restart: unless-stopped
env_file: .env
command:
- postgres
- -c
- shared_preload_libraries=pg_stat_statements
- -c
- track_activity_query_size=4096
- -c
- pg_stat_statements.track=all
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ['CMD-SHELL', 'pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}']
interval: 5s
timeout: 5s
retries: 10
# Autodiscovery del agente Datadog del host (lee las labels vía docker.sock).
labels:
com.datadoghq.ad.checks: |
{"postgres":{"init_config":{},"instances":[{"host":"%%host%%","port":5432,"username":"datadog","password":"%%env_DD_POSTGRES_PASSWORD%%","dbname":"%%env_POSTGRES_DB%%","dbm":true}]}}

redis:
image: redis:7
restart: unless-stopped
command: ['redis-server', '--save', '60', '1']
volumes:
- redisdata:/data
labels:
com.datadoghq.ad.checks: |
{"redisdb":{"init_config":{},"instances":[{"host":"%%host%%","port":6379}]}}

migrate:
image: postgres:16
env_file: .env
depends_on:
postgres:
condition: service_healthy
volumes:
- ../apps/api/drizzle:/migrations:ro
- ./migrate.sh:/migrate.sh:ro
entrypoint: ['sh', '/migrate.sh']
restart: 'no'

api:
build:
context: ..
dockerfile: apps/api/Dockerfile
restart: unless-stopped
env_file:
- path: .env
required: true
- path: .env.version
required: false
environment:
- DD_TRACE_ENABLED=${DD_TRACE_ENABLED:-false}
# Agente Datadog del host. 172.17.0.1 es la gateway de la bridge por
# defecto; verificar con: ip -4 addr show docker0
# El agente debe tener DD_APM_NON_LOCAL_TRAFFIC=true y escuchar en 0.0.0.0:8126.
- DD_AGENT_HOST=${DD_AGENT_HOST:-172.17.0.1}
- DD_TRACE_AGENT_PORT=8126
- DD_ENV=prod
- DD_SERVICE=responsegrid-api
- DD_LOGS_INJECTION=true
# El X-Forwarded-For lo pone nginx de Plesk.
- DD_TRACE_CLIENT_IP_ENABLED=true
depends_on:
migrate:
condition: service_completed_successfully
redis:
condition: service_started
# SOLO loopback: nginx de Plesk es quien expone el servicio al exterior.
ports:
- '127.0.0.1:3100:3000'

volumes:
pgdata:
redisdata:
20 changes: 20 additions & 0 deletions deploy/responsegrid.srv07.service
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Unit que levanta el stack en cada arranque de srv07.
# La instala el deploy (/usr/local/sbin/responsegrid-deploy) como
# /etc/systemd/system/responsegrid.service.
# Hace falta porque el contenedor `api` depende del one-shot `migrate`, y las
# políticas de restart de Docker por sí solas no reconcilian el stack al reiniciar.
[Unit]
Description=ResponseGrid stack (docker compose up on boot)
Requires=docker.service
After=docker.service network-online.target
Wants=network-online.target
ConditionPathExists=/opt/responsegrid/deploy/docker-compose.srv07.yml

[Service]
Type=oneshot
RemainAfterExit=yes
WorkingDirectory=/opt/responsegrid
ExecStart=/usr/bin/docker compose -p responsegrid -f deploy/docker-compose.srv07.yml up -d

[Install]
WantedBy=multi-user.target
60 changes: 60 additions & 0 deletions docs/deploy/srv07.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Producción actual: srv07 (Plesk + Docker)

La API corre en **srv07** (`srv07.ingenierosweb.co`), no en AWS. La guía de
[`aws-free-tier.md`](aws-free-tier.md) sigue siendo válida como alternativa
autocontenida, pero no describe el despliegue vivo.

La web (`API_URL` y `NEXT_PUBLIC_API_URL` en Vercel) apunta directamente al
subsitio. El dominio antiguo `api.responsegrid.app` es solo una *Redirect Rule*
308 de Cloudflare hacia él (conserva ruta y query), para consumidores externos y
para el callback de Google OAuth (`OAUTH_CALLBACK_BASE`), que sigue registrado con
ese dominio.

```
responsegrid.app (Vercel) ──► responsegrid-api.globalemergency.online
│ nginx de Plesk (TLS wildcard)
127.0.0.1:3100 → contenedor api
postgres · redis
```

## Diferencias con el stack de la EC2

| | EC2 (`docker-compose.prod.yml`) | srv07 (`docker-compose.srv07.yml`) |
|---|---|---|
| TLS y reverse proxy | Caddy en 80/443 | nginx de Plesk, la API solo escucha en `127.0.0.1:3100` |
| Datadog | contenedor `datadog/agent:7` propio | agente del host, trazas a `DD_AGENT_HOST` (gateway de docker0) |
| Credenciales de S3 | rol de instancia EC2 | usuario IAM con claves en `deploy/.env` |
| Arranque | `responsegrid.service` | igual, desde `deploy/responsegrid.srv07.service` |

El proyecto de compose se llama `responsegrid` (`-p responsegrid`) para no
colisionar con los demás stacks del servidor.

## Despliegue

Push a `main` → [`deploy.yml`](../../.github/workflows/deploy.yml) entra por SSH
a srv07. La clave `SRV07_DEPLOY_KEY` está atada a un *forced command* en
`/root/.ssh/authorized_keys`, así que solo puede ejecutar
`/usr/local/sbin/responsegrid-deploy`, que hace:

1. `git fetch` + `git reset --hard origin/main`
2. escribe `deploy/.env.version` con el sha corto (etiqueta `DD_VERSION`)
3. instala y habilita la unit de systemd
4. `docker compose -p responsegrid -f deploy/docker-compose.srv07.yml up -d --build`
(el servicio `migrate` aplica las migraciones pendientes antes de arrancar la API)
5. health check contra `http://127.0.0.1:3100/emergencies`; si no responde 200
en 2 minutos, falla el deploy y vuelca los logs de la API

Secretos del repo: `SRV07_HOST`, `SRV07_USER`, `SRV07_DEPLOY_KEY`.

## Operación manual

```bash
cd /opt/responsegrid
docker compose -p responsegrid -f deploy/docker-compose.srv07.yml ps
docker compose -p responsegrid -f deploy/docker-compose.srv07.yml logs api --tail 50
/usr/local/sbin/responsegrid-deploy # el mismo deploy, a mano
```

Backup diario de Postgres en `/etc/cron.daily/responsegrid-backup` →
`/var/backups/responsegrid`, retención 14 días.
Loading