# Guía de despliegue — Peninsula Fitness (Etapa 1)

Subida de prueba a un VPS para que el sitio + CRM funcionen **online con HTTPS**.
Pensado para **Ubuntu 22.04 / 24.04**. Tiempo estimado: ~30–45 min.

> 🟧 **¿VPS en Hostinger?** Primero haz los pasos propios de Hostinger
> (provisionar Ubuntu, SSH y apuntar el dominio) en **[`HOSTINGER-VPS.md`](./HOSTINGER-VPS.md)**
> y vuelve aquí a la **sección 3**.
>
> ⚠️ **No sirve el plan de Web Hosting / Cloud compartido (hPanel):** no corre Node
> SSR ni SQLite. Esta app requiere un **VPS** (KVM en Hostinger).

---

## 0. Cómo funciona (resumen de arquitectura)

```
Navegador ──HTTPS──> Nginx (80/443, TLS, rate-limit, gzip, cache estáticos)
                          │  proxy
                          ▼
                 App Node SSR (127.0.0.1:4321)         ← @astrojs/node "standalone"
                 gestionada por PM2 (autorestart/boot)
                          │
                          ▼
                 SQLite  .data/peninsula.sqlite        ← BD (fuente de verdad)
                 Uploads .data/uploads/products/       ← imágenes (fuera del root público)
```

- La app **solo escucha en localhost**; Nginx es la única cara pública.
- **Secretos** vía variables de entorno (`.env.production`), nunca en el repo.
- `better-sqlite3` es **nativo**: se compila *en el servidor* (por eso el deploy hace `npm ci` allí).
- Datos persistentes en `.data/` (BD + imágenes), **fuera** de `dist/` y de lo que sirve Nginx.

---

## 1. Requisitos

**En tu máquina (local):**
- El repositorio del proyecto.
- `ssh` y `rsync` (vienen en macOS/Linux).
- Una clave SSH (`ssh-keygen -t ed25519` si no tienes).

**Servicios:**
- Un **VPS** (1 vCPU / 1–2 GB RAM basta para Etapa 1) con Ubuntu y acceso root inicial.
- Un **dominio** (p. ej. `peninsulafitness.com.mx`) donde puedas editar DNS.

**Software que instala el script de setup (no tienes que hacerlo a mano):**
Node 20 LTS, build-essential (compilar SQLite), Nginx, PM2, Certbot, UFW, Fail2ban.

---

## 2. DNS

En tu proveedor de dominio crea dos registros **A** apuntando a la IP del VPS:

| Tipo | Nombre | Valor          |
|------|--------|----------------|
| A    | `@`    | `IP_DEL_VPS`   |
| A    | `www`  | `IP_DEL_VPS`   |

Verifica (puede tardar minutos en propagar):
```bash
dig +short peninsulafitness.com.mx
```

---

## 3. Preparar el servidor (una sola vez)

Entra como root y ejecuta el script de provisión/hardening:

```bash
ssh root@IP_DEL_VPS
# sube o pega deploy/scripts/server-setup.sh y:
sudo bash server-setup.sh
```

Esto instala todo, crea el usuario **`deploy`**, las carpetas `/var/www/peninsula` y
`.data/`, activa **UFW (22/80/443)** y **Fail2ban**, y deja PM2 listo para arrancar al boot.

> ⚠️ **Hardening SSH (hazlo tú, con cuidado):** copia tu clave
> `ssh-copy-id deploy@IP_DEL_VPS`, luego en `/etc/ssh/sshd_config` pon
> `PasswordAuthentication no` y `PermitRootLogin no`, y `sudo systemctl restart ssh`.
> **Confirma que puedes entrar como `deploy` con tu clave antes de cerrar root.**

---

## 4. Primer despliegue

### 4.1 Crear el archivo de secretos en el servidor
```bash
ssh deploy@IP_DEL_VPS
cd /var/www/peninsula
# (tras el primer rsync del paso 4.2 existirá el ejemplo; o créalo a mano)
cp deploy/env.production.example .env.production
nano .env.production         # pon ADMIN_PASSWORD fuerte, dominio, rutas
chmod 600 .env.production
```
Genera una contraseña fuerte con: `openssl rand -base64 24`.

### 4.2 Subir el código y compilar (desde tu LOCAL)
```bash
# en la raíz del proyecto, en tu máquina:
SERVER=deploy@IP_DEL_VPS ./deploy/scripts/deploy.sh
```
El script: valida build+tests local → `rsync` del código → en el servidor
`npm ci` + `npm run build` + `pm2 start/reload`.

> Si es la **primera vez** y aún no existe `.env.production`, el script te avisará:
> crea el archivo (4.1) y vuelve a ejecutar el deploy.

Comprueba que la app responde en local del servidor:
```bash
ssh deploy@IP_DEL_VPS 'curl -sI http://127.0.0.1:4321 | head -1'   # → HTTP/1.1 200 OK
ssh deploy@IP_DEL_VPS 'pm2 status'
```

---

## 5. Nginx + HTTPS

### 5.1 Instalar el sitio en Nginx
```bash
sudo cp /var/www/peninsula/deploy/nginx/peninsulafitness.conf \
        /etc/nginx/sites-available/peninsulafitness
sudo ln -s /etc/nginx/sites-available/peninsulafitness /etc/nginx/sites-enabled/
sudo rm -f /etc/nginx/sites-enabled/default
sudo nginx -t && sudo systemctl reload nginx
```
Ya deberías ver el sitio en `http://peninsulafitness.com.mx` (redirige a https, que aún no existe).

### 5.2 Emitir el certificado TLS (Certbot, automático)
```bash
sudo certbot --nginx -d peninsulafitness.com.mx -d www.peninsulafitness.com.mx
```
Certbot obtiene el certificado, **inyecta el bloque 443 + la redirección** y
programa la renovación automática. Verifica:
```bash
sudo certbot renew --dry-run
```

> El archivo de Nginx incluye, **comentado**, un bloque HTTPS "de referencia" con
> cache de `/_astro/`, `/media/`, `client_max_body_size 6m` y límite estricto en
> `/api/auth/`. Si gestionas TLS a mano, descoméntalo tras tener el certificado.
> Si usas Certbot, puedes añadir esas directivas dentro del server 443 que generó.

---

## 6. Backups automáticos

```bash
# en el servidor:
crontab -e
# añade (diario 3:15):
15 3 * * * /var/www/peninsula/deploy/scripts/backup.sh >> /var/log/peninsula/backup.log 2>&1
```
Respalda la BD (backup online consistente) + imágenes en `/var/backups/peninsula`,
conservando 14 copias. Prueba manual: `bash deploy/scripts/backup.sh`.

**Restaurar** (ejemplo): detén la app, copia el `db-FECHA.sqlite` sobre
`.data/peninsula.sqlite`, descomprime el `uploads-FECHA.tar.gz` en `.data/`, y `pm2 reload peninsula`.

---

## 7. Smoke test (verificación final online)

```bash
# Home con 200 y HTTPS
curl -sI https://peninsulafitness.com.mx | head -1

# Cabeceras de seguridad presentes
curl -sI https://peninsulafitness.com.mx | grep -iE 'strict-transport|content-security|x-frame|x-content-type'

# El CRM exige login (302 a /crm/login)
curl -sI https://peninsulafitness.com.mx/crm | grep -i location
```
Luego en el navegador: entra a `/crm/login` con `ADMIN_EMAIL` / `ADMIN_PASSWORD`,
crea un lead y una cotización, sube una imagen de producto y confirma que aparece
en `/productos`. Por último, corre Lighthouse contra la URL ya online:
```bash
npx lighthouse https://peninsulafitness.com.mx --preset=mobile --view
```

---

## 8. Actualizaciones (deploys siguientes)

Cada cambio se sube igual, con recarga **sin downtime**:
```bash
SERVER=deploy@IP_DEL_VPS ./deploy/scripts/deploy.sh
```

Comandos útiles en el servidor:
```bash
pm2 status
pm2 logs peninsula --lines 100
pm2 reload peninsula        # recarga
pm2 restart peninsula       # reinicio duro
```

---

## 9. Problemas comunes

| Síntoma | Causa probable | Solución |
|---|---|---|
| `502 Bad Gateway` | la app no corre o no escucha en 4321 | `pm2 status`, `pm2 logs peninsula`; revisa `.env.production` (PORT) |
| Error al `npm ci` con better-sqlite3 | faltan build tools | `sudo apt-get install -y build-essential python3` y reintenta |
| CRM no deja entrar / “sin admin” | `ADMIN_PASSWORD` vacío al primer arranque | ponlo en `.env.production`, `pm2 reload peninsula --update-env` |
| Imágenes subidas no cargan | rutas de datos mal configuradas | revisa `PF_UPLOADS_DIR` y permisos de `.data/` |
| Certbot falla | DNS no propaga o puerto 80 cerrado | `dig +short DOMINIO`, `sudo ufw status` (Nginx Full permitido) |
| Cambios no se reflejan | build viejo / caché | re-deploy; en estáticos, `Ctrl+Shift+R` |

---

## 10. Notas de seguridad (ya implementadas en la app)

- Cookies de sesión `HttpOnly` + `Secure` (en prod) + `SameSite=Lax`, con expiración en BD.
- **CSRF** (mismo origen) + **rate limiting** en auth/quote/uploads/mutaciones, con `Retry-After`.
- Cabeceras: CSP (con `frame-ancestors 'none'`), HSTS, COOP/CORP, `X-Content-Type-Options`, etc.
- **Subidas validadas** (tipo + magic bytes + 5 MB, máx. 12 por producto) **fuera del root público**.
- **Soft delete** por defecto y **auditoría** de toda mutación administrativa.
- El **rate-limit real por IP** lo aplica Nginx (`limit_req`); el de la app es defensa en profundidad.

> Recordatorio: la app es de **un solo proceso** (PM2 `fork`, 1 instancia) por SQLite y
> el rate-limiter en memoria. Para escalar horizontalmente: migrar a PostgreSQL
> (Drizzle ya está instalado) y mover el contador a Redis.
