# DOC-OPS-007 · Deploy en Cloud — face-match-node → apibatch.fidelitas.com.ar

**Producto:** Microservicio face-match-node · Fidelitas S.A.  
**Versión:** 1.3  
**Fecha:** 2026-11-10  
**Audiencia:** Developer Junior  
**Clasificación:** CONFIDENCIAL — USO INTERNO

---

## Índice

0. Deploy rápido — el servidor ya existe (re-deploy / actualización)
1. Resumen del proceso — qué se hace y qué NO se hace en el servidor
2. Prerequisitos — qué necesitás antes de empezar
3. Preparar el paquete en la estación de desarrollo
4. Acceder al servidor cloud por primera vez
5. Instalar Node.js 26 en el servidor (solo para ejecutar, no compilar)
6. Instalar PM2
7. Crear la estructura de carpetas en el servidor
8. Transferir el proyecto con rsync
9. Crear el .env de producción en el servidor
10. Arrancar el servicio con PM2
11. Instalar y configurar Apache con SSL
12. Configurar el VirtualHost para apibatch.fidelitas.com.ar
13. Verificar que todo funciona end-to-end
14. Configurar arranque automático con systemd
15. Troubleshooting — problemas frecuentes
16. Prueba del circuito completo — imagen real vs imagen ficticia
17. Rate limiting y stress test

---

## 0. Deploy rápido — el servidor ya existe (re-deploy / actualización)

Este es el flujo para cuando el servidor cloud ya está configurado y solo hay que subir cambios de código. **Todo desde tu máquina local.**

### 0.1 Verificar que el servidor está funcionando antes de tocar algo

```bash
curl -s https://apibatch.fidelitas.com.ar/health | python3 -m json.tool
# {"status":"ok","service":"fide-facematch-api","uptime":XXXX}
# Si esto no responde → ver sección 15 (Troubleshooting)
```

### 0.2 Recompilar si cambió package.json (omitir si solo cambió código fuente)

```bash
cd /home/memyself/Downloads/fide-workspace/face-match-node
./compile.sh
```

Si solo cambiaron archivos en `src/` (sin agregar/quitar dependencias), **no hace falta recompilar** — `node_modules` no cambió.

### 0.3 Transferir con rsync

```bash
# Desde la carpeta raíz del proyecto
cd /home/memyself/Downloads/fide-workspace/face-match-node

rsync -avz --progress \
  --exclude='.env' \
  --exclude='storage/' \
  --exclude='data/' \
  --exclude='logs/' \
  --exclude='stress_test.py' \
  . ubuntu@SERVER_IP:/var/www/html/apibatch/
```

> Reemplazar `SERVER_IP` con la IP real del servidor. Si usás clave `.pem`: agregar `-e "ssh -i ~/mi-clave.pem"` antes del punto final.

### 0.4 Reload sin downtime

```bash
# En el servidor — no corta conexiones activas
ssh ubuntu@SERVER_IP "pm2 reload fide-facematch"

# Verificar que arrancó correctamente
ssh ubuntu@SERVER_IP "pm2 status"
```

### 0.5 Verificar end-to-end

```bash
# Health check desde tu máquina
curl -s https://apibatch.fidelitas.com.ar/health

# Test de autenticación (debe rechazar con 401)
curl -s -X POST https://apibatch.fidelitas.com.ar/v3/face-match/ \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "apikey=INCORRECTA&selfie_image=abc&nationalid=12345678&gender=M"
```

**Eso es todo para un re-deploy.** Las secciones siguientes son para la instalación inicial desde cero.

---

## 1. Resumen del proceso — qué se hace y qué NO se hace en el servidor

### Regla fundamental

**El servidor cloud NO compila nada.** No tiene `build-essential`, no tiene `python3-dev`, no tiene `gcc`. Todo llega listo para ejecutar.

Lo que se transfiere desde la estación de desarrollo:

```
face-match-node/
├── src/                  ← código fuente
├── node_modules/         ← binarios ya compilados (Ubuntu 24.04 → compatible)
├── package.json
├── ecosystem.config.js
├── .env.example
├── .npmrc
└── compile.sh
```

Lo que NO se transfiere (se crea en el servidor):

```
.env          ← contiene credenciales — nunca viaja por red
storage/      ← se crea vacío en el servidor
data/         ← se crea vacío en el servidor (la DB se genera al primer arranque)
logs/         ← se crea vacío en el servidor
```

### Diagrama de lo que queda instalado

```
Internet
    │  HTTPS 443
    ▼
Apache (apibatch.fidelitas.com.ar)
    │  certbot/Let's Encrypt — TLS termina acá
    │  ProxyPass → localhost:3100
    ▼
face-match-node (PM2 cluster)
    │  puerto 3100 — solo escucha en 127.0.0.1
    ▼
RENAPER API (externa)
    + SQLite WAL (./data/fide-facematch.db)
    + storage/ (archivos por TCN)
```

---

## 2. Prerequisitos — qué necesitás antes de empezar

### En tu máquina local (estación de desarrollo)

| Qué | Por qué |
|---|---|
| `node_modules/` generado en este Ubuntu | Ya compilado — lo verificás con `ls node_modules/better-sqlite3/prebuilds/linux-x64.node` |
| Acceso SSH al servidor cloud | Clave privada SSH configurada |
| IP o hostname del servidor cloud | Te lo da el proveedor al crear la instancia |
| DNS configurado | `apibatch.fidelitas.com.ar` debe apuntar a la IP del servidor — configurarlo en el panel DNS del dominio **antes** de empezar |

### En el servidor cloud

| Qué | Por qué |
|---|---|
| Ubuntu Server 24.04 LTS | Sistema operativo de la instancia |
| Puerto 22 abierto | Para acceso SSH |
| Puerto 80 abierto | Para que Let's Encrypt valide el dominio |
| Puerto 443 abierto | Para HTTPS de producción |
| Usuario con sudo | Para instalar Apache y Node.js |

### Verificar DNS antes de continuar

Desde tu máquina local, verificar que el dominio ya apunta al servidor:

```bash
# Reemplazar SERVER_IP con la IP real del servidor cloud
dig apibatch.fidelitas.com.ar +short

# Debe devolver la IP del servidor
# Si devuelve otra IP o nada → configurar DNS primero y esperar propagación (5-30 min)
```

---

## 3. Preparar el paquete en la estación de desarrollo

### 3.1 Ir a la carpeta del proyecto

```bash
cd /home/memyself/Downloads/fide-workspace/face-match-node
```

### 3.2 Verificar que node_modules está compilado y completo

```bash
# Verificar binario nativo de SQLite (el más crítico)
ls node_modules/better-sqlite3/prebuilds/linux-x64.node
# Debe existir — si no, correr ./compile.sh primero

# Verificar que las 7 dependencias de producción están todas
node -e "
  ['fastify','@fastify/formbody','@fastify/helmet','@fastify/rate-limit',
   'better-sqlite3','undici','dotenv'].forEach(p => {
    require(p); console.log('OK:', p);
  });
"
```

Si todos los paquetes dicen OK, el `node_modules` está listo para transferir.

### 3.3 Verificar que el servidor local arranca sin errores

```bash
# Si ya está corriendo localmente, no hace falta relanzarlo
curl -s http://localhost:3100/health
# {"status":"ok","service":"fide-facematch-api","uptime":...}
```

---

## 4. Acceder al servidor cloud por primera vez

```bash
# Reemplazar SERVER_IP con la IP real del servidor
ssh ubuntu@SERVER_IP

# Si tu proveedor usa otro usuario (root, admin, ec2-user, etc.):
# ssh root@SERVER_IP
# ssh admin@SERVER_IP
```

Si el proveedor te da una clave privada `.pem` (AWS, Azure, etc.):

```bash
# Dar permisos correctos a la clave antes de usarla
chmod 400 ~/mi-clave-cloud.pem

# Conectarse con la clave
ssh -i ~/mi-clave-cloud.pem ubuntu@SERVER_IP
```

### 4.1 Verificar que el sistema operativo es el correcto

```bash
# Dentro del servidor:
lsb_release -d
# Debe mostrar: Description: Ubuntu 24.04.x LTS
```

### 4.2 Actualizar el sistema operativo

```bash
# Actualizar lista de paquetes disponibles
sudo apt-get update

# Instalar actualizaciones de seguridad disponibles
# El flag -y responde "sí" automáticamente a todas las preguntas
sudo apt-get upgrade -y
```

Esto puede tardar 2-5 minutos.

---

## 5. Instalar Node.js 26 en el servidor

Node.js se necesita **solo para ejecutar** el código JavaScript. No compila nada.

```bash
# Descargar e instalar el repositorio oficial de NodeSource para Node 20
curl -fsSL https://deb.nodesource.com/setup_26.x | sudo -E bash -

# Instalar Node.js (incluye npm automáticamente)
sudo apt-get install -y nodejs

# Verificar la instalación
node --version
# DEBE mostrar v26.x.x — si muestra otra versión, el repositorio no se aplicó bien

npm --version
# Debe mostrar 10.x.x
```

> **Importante:** la versión de Node.js en el servidor debe ser la misma rama ((26.x)) que se usó para generar el `node_modules` en la estación de desarrollo. Si son diferentes, los binarios nativos pueden fallar con un error `invalid ELF header` o similar.

---

## 6. Instalar PM2

PM2 es el gestor de procesos que mantiene el servicio corriendo en background, lo reinicia automáticamente si se cae, y distribuye la carga entre múltiples workers (uno por CPU core).

```bash
# Instalar PM2 de forma global
# El flag -g instala el paquete en /usr/lib/node_modules para que esté disponible como comando
sudo npm install -g pm2

# Verificar
pm2 --version
```

---

## 7. Crear la estructura de carpetas en el servidor

```bash
# Crear la carpeta principal del servicio
sudo mkdir -p /var/www/html/apibatch

# Dar permisos de escritura a tu usuario
# $USER es una variable que contiene tu nombre de usuario automáticamente
sudo chown $USER:$USER /var/www/html/apibatch

# Crear las subcarpetas que rsync va a excluir (no se transfieren)
mkdir -p /var/www/html/apibatch/storage
mkdir -p /var/www/html/apibatch/data
mkdir -p /var/www/html/apibatch/logs

# Verificar la estructura
ls -la /var/www/html/apibatch/
```

Deberías ver las 3 carpetas vacías.

---

## 8. Transferir el proyecto con rsync

Desde tu máquina local (NO desde el servidor), abrir una nueva terminal:

```bash
# Ir a la raíz del proyecto
cd /home/memyself/Downloads/fide-workspace/face-match-node

# rsync: transfiere solo lo que cambió, en modo comprimido
# node_modules/ SÍ se transfiere — ya está compilado para Ubuntu 24.04
# El punto final (.) indica "transferir el contenido de este directorio"
rsync -avz --progress \
  --exclude='.env' \
  --exclude='storage/' \
  --exclude='data/' \
  --exclude='logs/' \
  . \
  ubuntu@SERVER_IP:/var/www/html/apibatch/
```

**Con clave .pem:**

```bash
rsync -avz --progress \
  -e "ssh -i ~/mi-clave-cloud.pem" \
  --exclude='.env' \
  --exclude='storage/' \
  --exclude='data/' \
  --exclude='logs/' \
  . \
  ubuntu@SERVER_IP:/var/www/html/apibatch/
```

**Qué hace cada opción:**

| Opción | Qué hace |
|---|---|
| `-a` | Archive: preserva permisos, fechas, links simbólicos |
| `-v` | Verbose: muestra cada archivo que se transfiere |
| `-z` | Comprime durante la transferencia (más rápido en redes lentas) |
| `--progress` | Muestra el porcentaje y velocidad en tiempo real |
| `--exclude='.env'` | El .env NUNCA viaja por red — se crea manualmente en el servidor |
| `--exclude='storage/'` | Se creó vacío en el paso 7 |
| `--exclude='data/'` | La DB se genera sola al primer arranque |
| `--exclude='logs/'` | Se crea vacío en el paso 7 |

El `node_modules/` (con `better-sqlite3` ya compilado) **sí se transfiere**. Va a tardar 1-3 minutos dependiendo de la velocidad de la conexión.

### 8.1 Verificar que todo llegó

De vuelta en el servidor:

```bash
# Verificar que el binario nativo de SQLite llegó intacto
ls /var/www/html/apibatch/node_modules/better-sqlite3/prebuilds/linux-x64.node
# Debe existir

# Verificar estructura general
ls -la /var/www/html/apibatch/
```

---

## 9. Crear el .env de producción en el servidor

El `.env` se crea manualmente en el servidor. **Nunca viaja por red ni va en git.**

```bash
# En el servidor:
cp /var/www/html/apibatch/.env.example /var/www/html/apibatch/.env

# Editar con nano
nano /var/www/html/apibatch/.env
```

Completar con estos valores (reemplazar los que están en MAYÚSCULAS):

```bash
# Puerto interno — Apache hace proxy desde 443
PORT=3100

# Modo producción — desactiva logs de debug y activa optimizaciones
NODE_ENV=production

# API key de producción — generarla ejecutando este comando en el servidor:

APIKEY_MASTER=completar_con_el_valor_provisto_por_el_responsable

# IPs autorizadas en producción
# Poner las IPs de los sistemas que van a llamar al endpoint
# Si el consumidor es el portal de clientes u otro servicio en la misma red:
AUTHORIZED_IPS=["127.0.0.1","::1","IP_DEL_CONSUMIDOR"]

# RENAPER — completar con las credenciales reales de producción
RENAPER_TOKEN_URL=https://apirenaper.idear.gov.ar/CHUTROFINAL/API_ABIS/Autorizacion/token.php
RENAPER_TCN_URL=https://apirenaper.idear.gov.ar/CHUTROFINAL/API_ABIS/apiInline_v3.php
RENAPER_RESULT_URL=https://apirenaper.idear.gov.ar/CHUTROFINAL/API_ABIS/resultadoTCN.php
RENAPER_USERNAME=USUARIO_RENAPER_PRODUCCION
RENAPER_PASSWORD=PASSWORD_RENAPER_PRODUCCION

# Rutas absolutas (más seguras que relativas en producción)
STORAGE_DIR=/var/www/html/apibatch/storage
DB_PATH=/var/www/html/apibatch/data/fide-facematch.db

# Polling de resultado TCN
TCN_MAX_RETRIES=5
TCN_RETRY_DELAY_MS=500
```

Guardar con `Ctrl+O`, `Enter`, salir con `Ctrl+X`.

### 9.1 Proteger el archivo .env

```bash
# Solo el dueño puede leerlo — ningún otro usuario del sistema
chmod 600 /var/www/html/apibatch/.env

# Verificar
ls -la /var/www/html/apibatch/.env
# Debe mostrar: -rw------- (solo lectura/escritura para el dueño)
```

---

## 10. Arrancar el servicio con PM2

### 10.1 Verificar arranque manual primero

Antes de usar PM2, verificar que el servidor arranca sin errores de configuración:

```bash
cd /var/www/html/apibatch
node src/server.js
```

Respuesta esperada:

```json
{"level":30,"time":"...","msg":"Server listening at http://127.0.0.1:3100"}
```

Si hay error de variable de entorno faltante, el mensaje dice exactamente cuál. Corregir en el `.env` y reintentar.

Una vez que arranca sin errores, detener con `Ctrl+C`.

### 10.2 Arrancar con PM2 en modo cluster

```bash
cd /var/www/html/apibatch

# Arrancar en modo cluster con la configuración de producción
pm2 start ecosystem.config.js --env production

# Ver el estado de los workers
pm2 status
```

Verás una tabla como esta (una fila por CPU core del servidor):

```
┌─────┬────────────────────┬─────────┬────────┬───────────┬────────┐
│ id  │ name               │ mode    │ status │ cpu       │ memory │
├─────┼────────────────────┼─────────┼────────┼───────────┼────────┤
│ 0   │ fide-facematch     │ cluster │ online │ 0%        │ 45mb   │
│ 1   │ fide-facematch     │ cluster │ online │ 0%        │ 44mb   │
└─────┴────────────────────┴─────────┴────────┴───────────┴────────┘
```

### 10.3 Verificar que responde en el puerto interno

```bash
# Health check directo al servicio Node.js (sin pasar por Apache todavía)
curl http://localhost:3100/health
# {"status":"ok","service":"fide-facematch-api","uptime":5.2}
```

Si esto responde, el servicio Node.js está funcionando. Apache todavía no está configurado.

---

## 11. Instalar y configurar Apache con SSL

### 11.1 Instalar Apache

```bash
sudo apt-get install -y apache2

# Habilitar los módulos necesarios para hacer reverse proxy
# proxy: permite a Apache reenviar requests a otro servidor
sudo a2enmod proxy

# proxy_http: soporte específico para HTTP en el proxy
sudo a2enmod proxy_http

# headers: permite modificar headers HTTP (necesario para pasar la IP real del cliente)
sudo a2enmod headers

# ssl: soporte para HTTPS
sudo a2enmod ssl

# rewrite: permite redireccionar HTTP → HTTPS
sudo a2enmod rewrite

# Reiniciar Apache para que cargue todos los módulos
sudo systemctl restart apache2

# Verificar que está corriendo
sudo systemctl status apache2
# Debe mostrar: Active: active (running)
```

### 11.2 Instalar Certbot para el certificado SSL

Certbot es la herramienta oficial de Let's Encrypt para obtener certificados SSL gratuitos.

```bash
# Instalar Certbot y el plugin para Apache
sudo apt-get install -y certbot python3-certbot-apache
```

### 11.3 Obtener el certificado SSL

```bash
# Certbot va a verificar que apibatch.fidelitas.com.ar apunta a este servidor
# (por eso era importante configurar el DNS antes de este paso)
sudo certbot --apache -d apibatch.fidelitas.com.ar
```

Certbot va a hacer preguntas:
- **Email:** poner un email real (recibís alertas de expiración)
- **Terms of Service:** A (acepto)
- **Share email con EFF:** N (no es obligatorio)

Al terminar, Certbot crea automáticamente el certificado y configura Apache. El certificado se renueva automáticamente cada 90 días.

---

## 12. Configurar el VirtualHost para apibatch.fidelitas.com.ar

Certbot habrá creado un VirtualHost en `/etc/apache2/sites-enabled/`. Hay que agregar la configuración del reverse proxy.

### 12.1 Encontrar el archivo de configuración

```bash
# Listar los VirtualHosts activos
ls /etc/apache2/sites-enabled/

# Certbot normalmente crea un archivo con el nombre del dominio
# Ver el contenido para confirmar que es el correcto
sudo cat /etc/apache2/sites-enabled/apibatch.fidelitas.com.ar.conf
# o
sudo cat /etc/apache2/sites-enabled/000-default-le-ssl.conf
```

### 12.2 Crear el VirtualHost completo

En lugar de editar el que creó Certbot (puede ser confuso), crear uno nuevo limpio:

```bash
sudo nano /etc/apache2/sites-available/apibatch-fidelitas.conf
```

Pegar este contenido completo:

```apache
# ============================================================
# VirtualHost HTTP — redirige todo a HTTPS
# ============================================================
<VirtualHost *:80>
    ServerName apibatch.fidelitas.com.ar

    # Redirigir permanentemente todo el tráfico HTTP a HTTPS
    RewriteEngine On
    RewriteRule ^(.*)$ https://%{HTTP_HOST}$1 [R=301,L]
</VirtualHost>

# ============================================================
# VirtualHost HTTPS — proxy al microservicio Node.js
# ============================================================
<VirtualHost *:443>
    ServerName apibatch.fidelitas.com.ar

    # Logs de acceso y errores específicos para este dominio
    ErrorLog ${APACHE_LOG_DIR}/apibatch-error.log
    CustomLog ${APACHE_LOG_DIR}/apibatch-access.log combined

    # Certificado SSL (rutas generadas por Certbot)
    SSLEngine on
    SSLCertificateFile    /etc/letsencrypt/live/apibatch.fidelitas.com.ar/fullchain.pem
    SSLCertificateKeyFile /etc/letsencrypt/live/apibatch.fidelitas.com.ar/privkey.pem

    # Headers de seguridad adicionales
    Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
    Header always set X-Content-Type-Options "nosniff"
    Header always set X-Frame-Options "DENY"

    # ============================================================
    # Reverse proxy al microservicio face-match-node
    # El servicio escucha en 127.0.0.1:3100 (solo localhost — no expuesto)
    # ============================================================
    ProxyPreserveHost On

    # Pasar la IP real del cliente al servicio Node.js
    # (el servicio la usa para la validación de IP whitelist)
    RequestHeader set X-Forwarded-For "%{REMOTE_ADDR}s"
    RequestHeader set X-Real-IP "%{REMOTE_ADDR}s"
    RequestHeader set X-Forwarded-Proto "https"

    # Endpoint principal de face-match
    ProxyPass        /v3/face-match/ http://127.0.0.1:3100/v3/face-match/
    ProxyPassReverse /v3/face-match/ http://127.0.0.1:3100/v3/face-match/

    # Dashboard de estadísticas — protegido por IP en Node.js (DASHBOARD_ALLOWED_IPS)
    ProxyPass        /dashboard http://127.0.0.1:3100/dashboard
    ProxyPassReverse /dashboard http://127.0.0.1:3100/dashboard
    ProxyPass        /stats/ http://127.0.0.1:3100/stats/
    ProxyPassReverse /stats/ http://127.0.0.1:3100/stats/

    # Health check (para monitoreo y verificación)
    ProxyPass        /health http://127.0.0.1:3100/health
    ProxyPassReverse /health http://127.0.0.1:3100/health

    # Timeouts del proxy — ajustados para el polling de RENAPER
    # (5 retries × 500ms + latencia RENAPER = puede tardar hasta 10s)
    ProxyTimeout 30

</VirtualHost>
```

Guardar con `Ctrl+O`, `Enter`, salir con `Ctrl+X`.

### 12.3 Deshabilitar el VirtualHost por defecto y habilitar el nuevo

```bash
# Deshabilitar el VirtualHost por defecto (el que instaló certbot)
sudo a2dissite 000-default
sudo a2dissite 000-default-le-ssl 2>/dev/null || true
sudo a2dissite apibatch.fidelitas.com.ar.conf 2>/dev/null || true

# Habilitar el nuevo VirtualHost
sudo a2ensite apibatch-fidelitas.conf

# Verificar la sintaxis del archivo antes de recargar
# Si hay algún error de sintaxis, este comando lo muestra
sudo apache2ctl configtest
# Debe mostrar: Syntax OK
```

Si muestra `Syntax OK`, recargar Apache:

```bash
# reload: aplica la nueva configuración sin cortar conexiones activas
sudo systemctl reload apache2
```

---

## 13. Verificar que todo funciona end-to-end

Estos tests se hacen **desde tu máquina local** (no desde el servidor), probando a través de HTTPS como lo haría cualquier consumidor real.

### Test 1 — Health check (sin autenticación)

```bash
curl -s https://apibatch.fidelitas.com.ar/health | python3 -m json.tool
```

Respuesta esperada:

```json
{
    "status": "ok",
    "service": "fide-facematch-api",
    "uptime": 120.5
}
```

Si esto falla:
- **Could not resolve host** → el DNS no propagó todavía, esperar y reintentar
- **Connection refused** → Apache no está corriendo o el ProxyPass apunta al puerto equivocado
- **502 Bad Gateway** → Apache está corriendo pero Node.js no, verificar `pm2 status`

### Test 2 — Rechazo de apikey incorrecta (debe responder 401)

```bash
curl -s -X POST https://apibatch.fidelitas.com.ar/v3/face-match/ \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "apikey=INCORRECTA&selfie_image=abc&nationalid=12345678&gender=M" \
  | python3 -m json.tool
```

Respuesta esperada:

```json
{
    "Status": "ERROR",
    "Message": "API key inválida o no provista.",
    "Response": [],
    "Transaction_id": null
}
```

### Test 3 — Validación de campos (debe responder 400)

```bash
curl -s -X POST https://apibatch.fidelitas.com.ar/v3/face-match/ \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "apikey=TU_APIKEY_PRODUCCION&nationalid=12345678&gender=M" \
  | python3 -m json.tool
```

Respuesta esperada (HTTP 400): error indicando que falta `selfie_image`.

### Test 4 — Request completo con imagen y credenciales RENAPER

```bash
# Preparar la imagen de prueba
cd /home/memyself/Downloads/fide-workspace/face-match-node

# Enviar el request completo
curl -s -X POST https://apibatch.fidelitas.com.ar/v3/face-match/ \
  -H "Content-Type: application/x-www-form-urlencoded" \
  --data-urlencode "apikey=TU_APIKEY_PRODUCCION" \
  --data-urlencode "selfie_image=$(cat storage/selfie_test_b64.txt)" \
  --data-urlencode "nationalid=22222222" \
  --data-urlencode "gender=M" \
  | python3 -m json.tool
```

Respuesta exitosa esperada:

```json
{
    "Status": "OK",
    "Message": "OK",
    "Response": [
        [{"tcnid": "TCN-...", "status": "NO.HIT", "score": "...", ...}],
        [{"name": "", "surname": "", ...}]
    ],
    "Transaction_id": 71200081
}
```

### Test 5 — Verificar que la DB se pobló

```bash
# En el servidor
node -e "
const db=new(require('/var/www/html/apibatch/node_modules/better-sqlite3'))('/var/www/html/apibatch/data/fide-facematch.db');
console.table(db.prepare('SELECT id,tcn_id,national_id,status,score,time_total_ms,created_at FROM face_match_results ORDER BY id DESC LIMIT 5').all());
"
```

### Test 6 — Verificar que los archivos se guardaron

```bash
ls -la /var/www/html/apibatch/storage/
# Debe mostrar los 3 archivos por cada operación exitosa:
# TCN-xxx.txt  TCN-xxx_step1.json  TCN-xxx_step2.json
```

---

## 14. Configurar arranque automático con systemd

Este paso hace que PM2 (y por lo tanto el servicio) se reinicie automáticamente cuando el servidor cloud se reinicia (por mantenimiento, actualizaciones, cortes de energía, etc.).

```bash
# En el servidor — generar el script de startup para systemd
# El comando imprime en pantalla el comando sudo que hay que ejecutar
pm2 startup systemd -u $USER --hp $HOME
```

El comando imprime algo como:

```
[PM2] You have to run this command as root. Execute the following command:
sudo env PATH=$PATH:/usr/bin /usr/lib/node_modules/pm2/bin/pm2 startup systemd -u ubuntu --hp /home/ubuntu
```

**Copiar ese comando exacto y ejecutarlo:**

```bash
# Pegar el comando que imprimió pm2 startup (el tuyo puede ser diferente)
sudo env PATH=$PATH:/usr/bin /usr/lib/node_modules/pm2/bin/pm2 startup systemd -u ubuntu --hp /home/ubuntu
```

Luego guardar el estado actual de PM2 para que se restaure al reiniciar:

```bash
pm2 save
```

### 14.1 Verificar que el arranque automático está configurado

```bash
# El servicio PM2 debe estar habilitado en systemd
sudo systemctl status pm2-$USER
# Debe mostrar: Active: active (running)
# y: Loaded: loaded (/etc/systemd/system/pm2-ubuntu.service; enabled)
```

### 14.2 Probar el arranque automático (opcional pero recomendado)

```bash
# Reiniciar el servidor para verificar que todo arranca solo
sudo reboot

# Esperar 30-60 segundos y volver a conectar
ssh ubuntu@SERVER_IP

# Verificar que PM2 arrancó automáticamente
pm2 status

# Verificar que el servicio responde
curl http://localhost:3100/health
```

---

## 15. Troubleshooting — problemas frecuentes

### El health check no responde desde internet

```bash
# Verificar que Apache está corriendo
sudo systemctl status apache2

# Verificar que Node.js está corriendo
pm2 status

# Verificar que Node.js responde localmente
curl http://localhost:3100/health

# Ver los últimos errores de Apache
sudo tail -50 /var/log/apache2/apibatch-error.log

# Ver los logs de PM2
pm2 logs fide-facematch --lines 50
```

### Error 502 Bad Gateway

Apache está corriendo pero no puede llegar al servicio Node.js.

```bash
# Verificar que Node.js está en el puerto 3100
ss -tlnp | grep 3100
# Debe mostrar: LISTEN 127.0.0.1:3100

# Si no muestra nada, el servicio no está corriendo
pm2 restart fide-facematch
```

### Error "invalid ELF header" al arrancar

El `node_modules` fue compilado en un OS diferente al del servidor.

```bash
# En la estación de desarrollo, regenerar el node_modules para Ubuntu 24.04
cd /home/memyself/Downloads/fide-workspace/face-match-node
./compile.sh

# Volver a transferir con rsync
rsync -avz --progress \
  --exclude='.env' --exclude='storage/' --exclude='data/' --exclude='logs/' \
  . ubuntu@SERVER_IP:/var/www/html/apibatch/

# Reiniciar el servicio
pm2 restart fide-facematch
```

### El certificado SSL no se pudo obtener

```bash
# Verificar que el DNS apunta correctamente al servidor
dig apibatch.fidelitas.com.ar +short
# Debe mostrar la IP del servidor

# Verificar que el puerto 80 está abierto (Certbot lo necesita para validar)
curl -v http://apibatch.fidelitas.com.ar

# Reintentar obtener el certificado
sudo certbot --apache -d apibatch.fidelitas.com.ar
```

### La DB no se crea / está vacía

```bash
# Verificar que el directorio data existe y tiene permisos correctos
ls -la /var/www/html/apibatch/data/
# El dueño debe ser tu usuario, no root

# Si el dueño es root, corregir
sudo chown $USER:$USER /var/www/html/apibatch/data/

# Reiniciar el servicio
pm2 restart fide-facematch

# Hacer un request completo exitoso — la DB se crea en el primer request que llega
# al handler (no al arranque)
```

### Ver logs en tiempo real durante una prueba

```bash
# En una terminal del servidor:
pm2 logs fide-facematch

# En otra terminal, hacer el request de prueba
# Los logs muestran cada evento en tiempo real
```

---

## Resumen de comandos de operación diaria en el servidor cloud

```bash
# Estado
pm2 status
pm2 monit                              # monitor interactivo CPU/RAM

# Logs
pm2 logs fide-facematch                # tiempo real
pm2 logs fide-facematch --lines 200    # últimas 200 líneas

# Reinicio
pm2 reload fide-facematch    # zero-downtime (para deploys)
pm2 restart fide-facematch   # reinicio completo

# Apache
sudo apache2ctl configtest
sudo systemctl reload apache2
sudo tail -50 /var/log/apache2/apibatch-error.log

# Health check
curl https://apibatch.fidelitas.com.ar/health

# DB — últimas 10 operaciones
node -e "
const db=new(require('/var/www/html/apibatch/node_modules/better-sqlite3'))('/var/www/html/apibatch/data/fide-facematch.db');
console.table(db.prepare('SELECT id,tcn_id,national_id,status,score,time_total_ms,created_at FROM face_match_results ORDER BY id DESC LIMIT 10').all());
"
```

---

## 16. Prueba del circuito completo — imagen real vs imagen ficticia

Una vez que el servicio está corriendo en el cloud con credenciales RENAPER reales, hacer las siguientes dos pruebas para validar que el circuito end-to-end funciona correctamente.

### 16.1 Condiciones de la selfie real

Tomarse una foto con el celular o cámara cumpliendo estas condiciones:

| Requisito | Detalle |
|---|---|
| Fondo | Liso y claro (blanco o gris) — sin objetos detrás |
| Posición | Frontal, rostro centrado en el encuadre |
| Iluminación | Uniforme, sin sombras sobre la cara |
| Expresión | Neutra, boca cerrada, ojos abiertos |
| Lentes / sombrero | Sin ninguno |
| Formato | JPEG |
| Resolución mínima | 400×400 px |
| Tamaño máximo | 5 MB |

Transferir la foto a la PC y convertirla a base64:

```bash
# Reemplazar selfie_real.jpg con el nombre real de la foto
base64 -w 0 selfie_real.jpg > selfie_real_b64.txt

# Verificar tamaño — no debe superar ~7 MB en base64
wc -c selfie_real_b64.txt
```

### 16.2 Prueba con imagen real — debe dar HIT o NO.HIT

```bash
curl -s -X POST https://apibatch.fidelitas.com.ar/v3/face-match/ \
  -H "Content-Type: application/x-www-form-urlencoded" \
  --data-urlencode "apikey=TU_APIKEY" \
  --data-urlencode "selfie_image=$(cat selfie_real_b64.txt)" \
  --data-urlencode "nationalid=TU_DNI" \
  --data-urlencode "gender=M" \
  | python3 -m json.tool
```

Respuesta esperada con imagen válida:

```json
{
    "Status": "OK",
    "Message": "OK",
    "Response": [
        [{
            "tcnid": "TCN-...",
            "status": "HIT",
            "score": "...",
            "face_match_datetime": "2026-11-07 ...",
            "national_id": "TU_DNI",
            "facematch_id": "TCN-..."
        }],
        [{"name": "", "surname": "", ...}]
    ],
    "Transaction_id": ...
}
```

- `status: "HIT"` → el rostro coincide con el DNI en RENAPER
- `status: "NO.HIT"` → no coincide
- `score` → puntaje de similitud — a mayor valor, mayor coincidencia

### 16.3 Preparar la imagen ficticia

Sirve para verificar que RENAPER evalúa la imagen y devuelve `NO.HIT` — confirma que el rechazo es del motor de reconocimiento y no un error del servicio.

Generar una imagen sintética con ImageMagick (ya instalado en Ubuntu):

```bash
convert \
  -size 400x500 xc:'#f0e6d2' \
  -fill '#d4a574' -draw "ellipse 200,220 100,130 0,360" \
  -fill '#1a0a00' -draw "ellipse 160,190 15,10 0,360" \
  -fill '#1a0a00' -draw "ellipse 240,190 15,10 0,360" \
  -fill '#8b5e3c' -draw "ellipse 200,250 25,8 0,360" \
  -quality 85 \
  selfie_ficticia.jpg

base64 -w 0 selfie_ficticia.jpg > selfie_ficticia_b64.txt
```

### 16.4 Prueba con imagen ficticia — debe dar NO.HIT o error de calidad

```bash
curl -s -X POST https://apibatch.fidelitas.com.ar/v3/face-match/ \
  -H "Content-Type: application/x-www-form-urlencoded" \
  --data-urlencode "apikey=TU_APIKEY" \
  --data-urlencode "selfie_image=$(cat selfie_ficticia_b64.txt)" \
  --data-urlencode "nationalid=22222222" \
  --data-urlencode "gender=M" \
  | python3 -m json.tool
```

Con una imagen ficticia RENAPER puede devolver:
- `NO.HIT` con score bajo → no encontró coincidencia (comportamiento normal)
- Error de calidad → RENAPER rechazó la imagen por no detectar rostro

Ambas respuestas son correctas — confirman que el motor de RENAPER está evaluando.

### 16.5 Verificar ambas operaciones en la DB

```bash
node -e "
const db=new(require('/var/www/html/apibatch/node_modules/better-sqlite3'))
  ('/var/www/html/apibatch/data/fide-facematch.db');
console.table(db.prepare(
  'SELECT tcn_id, national_id, status, score, time_total_ms, created_at FROM face_match_results ORDER BY id DESC LIMIT 5'
).all());
"
```

Deberías ver las dos operaciones con sus TCN, status y score distintos.

---

## 17. Rate limiting y stress test

### 17.1 Configurar el rate limiting

El rate limiting tiene **dos parámetros** en el archivo `.env` del servidor — no hay que tocar código:

| Variable | Valor por defecto | Qué controla |
|---|---|---|
| `RATE_LIMIT_PER_IP_PER_MIN` | 600 | Requests por minuto por IP individual (protección anti-abuso) |
| `RATE_LIMIT_GLOBAL_PER_MIN` | 30000 | Techo global del servicio (500 rps × 60s) — **negociar con RENAPER antes de subir** |

Para cambiar el límite, editar el `.env` en el servidor y recargar sin downtime:

```bash
# En el servidor:
nano /var/www/html/apibatch/.env
# Editar RATE_LIMIT_PER_IP_PER_MIN y/o RATE_LIMIT_GLOBAL_PER_MIN

pm2 reload fide-facematch
```

### 17.2 El cuello de botella real es RENAPER

El servicio Node.js puede manejar 500 rps sin problema. El limitante real es RENAPER: cada request hace **2 llamadas HTTP externas síncronas** (TCN + resultado). El pool de conexiones hacia RENAPER está en `src/services/renaperService.js`:

```javascript
const renaperAgent = new Agent({
  connections: 50,  // máximo 50 llamadas simultáneas a RENAPER
});
```

**Regla:** aumentar `connections` solo después de confirmar con RENAPER cuánta concurrencia permite. Si se supera, RENAPER empieza a devolver errores de throttling.

### 17.3 Stress test Python — stress_test.py (recomendado)

El script `stress_test.py` en la raíz del proyecto es la herramienta principal para medir la infraestructura. Mide el stack completo con precisión de RPS, registra cada transacción en SQLite y emite un resumen de KPIs al final.

**Prerequisito:** instalar `aiohttp` si no está instalado:

```bash
pip3 install aiohttp --break-system-packages
```

#### Test de infraestructura local (sin credenciales RENAPER)

```bash
cd /home/memyself/Downloads/fide-workspace/face-match-node

# Standard: 200 RPS × 60s = 12.000 requests
python3 stress_test.py

# Test rápido de verificación
python3 stress_test.py --rps 50 --duration 10
```

#### Test contra el cloud

```bash
# Desde tu máquina local — mide el stack completo (Internet + Apache + TLS + Node.js)
python3 stress_test.py \
  --url https://apibatch.fidelitas.com.ar \
  --apikey TU_APIKEY_PRODUCCION \
  --rps 100 \
  --duration 60

# Desde el servidor cloud — mide solo Node.js (sin overhead de red ni TLS)
# (copiar stress_test.py al servidor primero)
python3 stress_test.py \
  --url http://localhost:3100 \
  --rps 200 \
  --duration 60 \
  --db /var/www/html/apibatch/data/fide-facematch.db
```

> **Atención:** un test de alta carga contra producción con credenciales RENAPER reales consume quota. Coordinar con el equipo antes.

#### Qué mirar en los resultados

| Métrica | Qué indica | Valor de alerta |
|---|---|---|
| `RPS real` | Throughput real del servidor | < 90% del objetivo = servidor saturado |
| `Errores de red` | Conexiones rechazadas o timeout | > 0 |
| `Latencia p99` | Peor caso del 99% de los usuarios | > 3s con RENAPER real = problema |
| `HTTP 429` | Rate limit activado | Subir `RATE_LIMIT_PER_IP_PER_MIN` en `.env` |
| `HTTP 200` | Requests exitosos a RENAPER | 100% con credenciales reales |
| `HTTP 500/502` | Falla RENAPER | Esperado sin credenciales; en prod = problema |

#### Consultar los KPIs registrados en SQLite después del test

```bash
# En el servidor — resumen del último run
sqlite3 /var/www/html/apibatch/data/fide-facematch.db \
  "SELECT test_run_id, COUNT(*) AS n,
          ROUND(AVG(response_ms),0) AS avg_ms,
          MAX(response_ms) AS max_ms,
          SUM(CASE WHEN http_status=200 THEN 1 END) AS ok_200,
          SUM(CASE WHEN http_status=429 THEN 1 END) AS rate_limited
   FROM load_test_results
   GROUP BY test_run_id
   ORDER BY MIN(created_at) DESC
   LIMIT 5;"
```

Ver [DOC-OPS-008_stress_test.md](DOC-OPS-008_stress_test.md) para la documentación completa del script: todos los parámetros, consultas SQLite de KPIs, monitoreo en tiempo real y cómo interpretar cada resultado.

### 17.4 Test rápido bash — load_test.sh (alternativa simple)

Script bash sin dependencias. Sirve para verificaciones rápidas sin instalar Python.

```bash
cd /home/memyself/Downloads/fide-workspace/face-match-node

# 50 rps × 5s (lee configuración del .env)
bash load_test.sh

# Contra el cloud
LOAD_TEST_URL=https://apibatch.fidelitas.com.ar bash load_test.sh

# 100 rps × 10s
LOAD_TEST_RPS=100 LOAD_TEST_DURATION=10 bash load_test.sh
```

Limitación: `load_test.sh` no registra en SQLite ni calcula percentiles con la misma precisión que `stress_test.py`. Usar `stress_test.py` para tests formales de infraestructura.

---

*DOC-OPS-007 · v1.3 · 2026-11-10 · CONFIDENCIAL — USO INTERNO · Fidelitas S.A.*
