Fase 5: Servidor VPS

Contenidos

  • Rutas dinámicas en Astro
  • Servicio fail2ban
  • Firewall ufw

Rutas dinámicas en Astro

  • ✅ Hasta ahora hemos creado rutas estáticas → Accedes a una ruta directamente
  • ❌ No vamos a crear todas las rutas de cada juego (¿y si tengo 300 juegos?)
  • ✅ Lo que haremos es indicarle a Astro:
    • ...todos los juegos que hay
    • ...una plantilla dinámica
    • ✅ El revisará todos los juegos y la plantilla, y creará una página por cada uno

  • Si intentamos hacer una ruta estática con los juegos, nos aparecerá...
    • ❌ ERROR: getStaticPaths() function required for dynamic routes.

Las rutas dinámicas: getStaticPaths()

---
export function getStaticPaths() {
  return [
    { params: { slug: "beneath-steel-sky" }},
    { params: { slug: "gabriel-knight" }}
  ];
}

const { slug } = Astro.params;

---

{slug}
  • 📁 Vamos a la carpeta src/pages/games/
  • 📄 Renombramos index.astro a [slug].astro
    • Los corchetes indican que es página dinámica
    • El nombre del interior es la variable implicada
    • Para la URL /games/monkey-island...
      • slug va a contener monkey-island

  • 🟩 Exportamos la función getStaticPaths()
    • Devolvemos un Array donde tiene un objeto por página
    • /games/beneath-steel-sky✅
    • /games/gabriel-knight✅
    • /games/monkey-island❌
    • /games/broken-sword❌
---
import GAMES from "@/data/games.json";

export function getStaticPaths() {
  return GAMES.map(game => ({ params: { slug: game.slug } }));
}

// Procesamiento por página
const { slug } = Astro.params;
const data = GAMES.find(game => game.slug === slug);
---

<pre>{JSON.stringify(data)}</pre>

  • 📁 Importamos desde @/data/games.json
  • 🟩 En la función getStaticPaths()
    • Recorremos GAMES y devolvemos el objeto...
    • ➡ { params: { slug: "SLUG-DEL-JUEGO" } }
  • 🟪 En cada página, data...
    • Guarda los datos del juego en cuestión

  • Ahora podemos acceder a todos los juegos:
    • /games/beneath-steel-sky✅
    • /games/gabriel-knight✅
    • /games/monkey-island✅
    • /games/broken-sword✅

Seguridad

Seguridad: Firewall (Digital Ocean)

  • Firewall a nivel de red → Se bloquea antes de llegar al VPS
  • Networking / Firewalls / Create Firewall
    • → Por defecto, in ❌, out ✅
  • 🟦 Add inbound rules → SSH (22)
  • ✅ Add inbound rules → HTTP (80)
  • ✅ Add inbound rules → HTTPS (443)
  • ✅ Add inbound rules → MySQL (3306) *
  • 🟦 Add outbound rules → ICMP (ping)
  • 🟦 Add outbound rules → TCP (all ports)
  • 🟦 Add outbound rules → UDP (all ports)
  • Apply to DropletsSeleccionar droplets

    * Acceder directamente
    ** Cloudflare también tiene su propio firewall


Seguridad: Firewall

  • Firewall por software, ufw → uncomplicated firewall → front de iptables/nftables
sudo apt install ufw
sudo ufw status verbose
Status: inactive            # Por defecto, inactivo

sudo ufw default deny incoming    # entrada ❌
sudo ufw default allow outgoing   # salida ✅
sudo ufw allow 22/tcp             # puedes usar "ssh" en vez de "22/tcp"

sudo ufw enable                   # activar
  • Si te bloqueas por error, entrar en DigitalOcean a través de su panel y quitarte el "ban"
sudo ufw allow 80/tcp         # HTTP
sudo ufw allow 443/tcp        # HTTPS
sudo ufw status numbered      # Muestra las reglas numeradas
Status: active

     To                         Action      From
     --                         ------      ----
[ 1] 22/tcp                     ALLOW       Anywhere
[ 2] 80/tcp                     ALLOW       Anywhere
[ 3] 443/tcp                    ALLOW       Anywhere
[ 4] 22/tcp (v6)                ALLOW       Anywhere (v6)
[ 5] 80/tcp (v6)                ALLOW       Anywhere (v6)
[ 6] 443/tcp (v6)               ALLOW       Anywhere (v6)
# Eliminar/Insertar
sudo ufw delete 4                                # Borra la regla número 4
sudo ufw insert 1 deny from 88.44.12.50          # Bloquea IP
sudo ufw insert 1 allow from 11.22.33.44         # Permite IP

# Permitir/Bloquear
sudo ufw allow from 11.22.33.44 to any port 22   # Permite IP sólo por ssh
sudo ufw deny from 88.22.33.44 to any port 443   # Bloquea IP sólo por ssh
sudo ufw deny from 88.44.33.0/24                 # Bloquea IP 88.44.33.x (254 ips)
sudo ufw deny from 88.44.0.0/16                  # Bloquea IP 88.44.x.x (65K ips)

# Limitar
sudo ufw limit 22/tcp                            # Si una IP intenta +6/30seg, bloquea

Seguridad: Fail2ban

  • Fail2ban permite banear varios intentos fallidos (de múltiples formas)
sudo apt install fail2ban
sudo fail2ban-client status
|- Number of jail:      1
`- Jail list:   sshd

tree /etc/fail2ban
📂 fail2ban/
├── 📂 action.d/           # Acciones: Qué hacemos
├── 📂 fail2ban.d/         # Fragmentos de configuración
├── 📂 filter.d/           # Filtros: Qué detectamos
├── 📂 jail.d/             # Jaula: Servicio + Filtro + Acción
├── 📄 fail2ban.conf       # Configuración general
└── 📄 jail.conf           # Configuración de jaulas
  • Configuración principal de fail2ban (no se toca)
egrep ^[^#] /etc/fail2ban/fail2ban.conf           # No modifiques nada

[DEFAULT]
loglevel = INFO                                   # Nivel de error: DEBUG > INFO > ERROR
logtarget = /var/log/fail2ban.log                 # Logs del sistema
syslogsocket = auto
socket = /var/run/fail2ban/fail2ban.sock
pidfile = /var/run/fail2ban/fail2ban.pid
dbfile = /var/lib/fail2ban/fail2ban.sqlite3       # Base de datos de IPs baneadas
dbpurgeage = 1d                                   # Cada cuanto se borran datos de la DB
dbmaxmatches = 10
  • Acciones que podemos usar: /etc/fail2ban/action.d/
ls /etc/fail2ban/action.d

abuseipdb.conf        # Reporta a AbuseIP una IP que está haciendo uso indebido
cloudflare.conf       # Bloquea en el WAF/Firewall de Cloudflare la IP atacante
hostsdeny.conf        # Bloquea con hostsdeny (alternativa simple a fail2ban)
??tables*.conf        # Bloquea mediante iptables/nftables
mail.conf             # Informa por email de la IP que está atacando
shorewall.conf        # Bloquea mediante ShoreWall
ufw.conf              # Bloquea mediante ufw (la que usaremos)
[...]

egrep "^[^#]" abuseipdb.conf | ccze -A
  • Filtros que podemos usar: /etc/fail2ban/filter.d/
ls /etc/fail2ban/filter.d

apache-badbots.conf       # Bots de spam o malware que realizan peticiones
botsearch-common.conf     # Bots que buscan webmail/phpmyadmin/wordpress/mysqladmin...
nginx-forbidden.conf      # Bots que acceden a recursos prohibidos continuamente
nginx-*.conf              # Otros filtros relacionados con nginx
sshd.conf                 # Accesos prohibidos a SSH (el que usaremos)

egrep "^[^#]" apache-badbots.conf | ccze -A
  • En /etc/fail2ban/jail.d/ creamos nuestras jaulas → Por defecto, defaults-debian.conf
[DEFAULT]
banaction = nftables                              # Cual es la acción por defecto
banaction_allports = nftables[type=allports]      # Cambiaremos `nftables` → `ufw`

[sshd]                                                  # Nombre del jail
backend = systemd                                       # Como lee los logs fail2ban
journalmatch = _SYSTEMD_UNIT=ssh.service + _COMM=sshd   # Logs de servicio, proceso sshd
enabled = true                                          # Activo por defecto
sudo fail2ban-client -t          # Comprobamos que no hay errores de sintaxis
sudo journalctl -u fail2ban -f   # Ver últimos registros del log (de fail2ban)
sudo ufw status numbered         # Revisar periódicamente los bloqueos añadidos en ufw

Node en VPS

Gestión de archivos en remoto

  • Entorno local → Entorno remoto (ssh)
  • cpscp (cp vía ssh)
  • cp /home/manz/folder/* /home/manz/folder2/ → (cp origen destino)
  • cp /home/manz/folder/* manz@111.22.33.44:/home/server/folder/ → (cp origen user@IP:/destino/)
  • cp manz@111.22.33.44:/home/server/folder/* /home/manz/folder3/ → (cp user@IP:/origen/ destino)

  • Consejo: Crea un script
    • pnpm run build → Genera una carpeta con la web lista para distribuir
    • pnpm run deploy → Podría ser un atajo a un script que realice un scp (entre otras cosas)

  • En el futuro, lo ideal es integrar esto en un workflow de CI/CD (GitHub Actions, etc...)
  • Recuerda que lo ideal es tener una key SSH en tu servidor.
  • Si la has generado, al hacer un ssh o un scp usa el parámetro -i ~/.ssh/mi_sshkey
  • Protip: Puedes automatizarlo:
nano ~/.ssh/config

Host nombre-server
  HostName dominio.com
  User manz
  IdentityFile ~/.ssh/mi_sshkey
  Port 22

Objetivo: Configurar nginx con backend node

  • Vamos a hacer lo siguiente:
    • droplet de DO escuchando
      • En :80 (HTTP)
      • En :443 (HTTPS)
    • nginx escucha peticiones dentro
      • Si son requests estáticos, los sirve
      • Si no lo son, redirecciona a node
    • pm2 escucha en :3000 (solo accesible via nginx)
      • pm2 está residente y escuchando
      • pm2 redirige a node

  • MEJOR: Controlar bien las peticiones que llegan a node (lo veremos más adelante)

Configuramos nginx con node

# Aquí es donde va a estar escuchando PM2 (Node)
upstream backend {
  server 127.0.0.1:3000;
}

# Escuchamos los estáticos y sino, enviamos a @backend
server {
  listen 80;
  server_name _;

  # Servidor de estáticos
  location / {
    root /var/www/domain.com/public/;
    try_files $uri $uri/ @backend;
  }                    # ^ Aunque coincide el nombre
                       # no tiene que ver con el upstream
  # ...                # (solo conecta con el location)
}
server {
  # ...

  location @backend {
    # Redirigimos al backend
    proxy_pass http://backend;      # Este SÍ conecta con el upstream

    # Reescritura de encabezados originales
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header Host $http_host;

    # Compatibilidad con websockets
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_cache_bypass $http_upgrade;
  }
}

Configurar pm2

  • Ahora necesitamos que node escuche en :3000
  • Para ello, vamos a instalar pm2
  • PNPM ocupa bastante y tenemos un VPS pequeño, vamos a ser selectivos
sudo apt install nodejs npm       # ❌ NO HACER. Ocupa bastante (~1GB)

# Añadimos soporte oficial para Node 24 (LTS)
curl -fsSL https://deb.nodesource.com/setup_24.x | sudo bash -
sudo apt install nodejs
node --version            # 24+
npm --version             # 11+
sudo npm install -g pm2   # CUIDADO: Usar sudo solo aquí porque es global
pm2 -v                    # 7+
cd /var/www/dominio.com
mkdir public
mv * public
mkdir api
cd api
npm init -y --init-type module
npm install express

nano /var/www/dominio.com/api/index.js
import express from "express";
const app = express();

const HOST = "localhost";   // Sueles necesitar cambiarlo a 0.0.0.0
const PORT = 3000;

app.get("/api/", (req, res) => res.send("API works!"));
app.get("/api/list", (req, res) => res.send("List of API"));
app.get("/api/about", (req, res) => res.send("About"));

app.listen(PORT, HOST, () => {
  console.log(`Server running at http://localhost:${PORT}/`);
});
upstream backend {
  server 127.0.0.1:3000;
}

server {
  # ...
  root /var/www/dominio.com/public;
  # ...
}
server {
  # ...
  location /api {
    limit_req zone=api burst=5 nodelay;
    proxy_pass http://backend;

    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header Host $http_host;

    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_cache_bypass $http_upgrade;
  }
}
# Esto arranca el servidor de node (manualmente)
node /var/www/dominio.com/api/index.js

# Pero nos interesa que esto se haga solo, no manualmente
pm2 start /var/www/dominio.com/api/index.js --name api-server

# Revisamos y hacemos que los cambios sean permanentes
curl localhost:3000
pm2 save
pm2 status                 # monit
pm2 restart api-server     # stop, delete
pm2 logs

Final del Bootcamp

¿Y ahora qué?

  • Repaso rápido a lo aprendido
  • Próximos pasos
  • Feedback de los alumnos
  • Próximo Bootcamp: IA agéntica
  • Despedida

Pasos futuros

  • 👨‍💻 Seguir prácticando (esto no termina aquí)
  • 🟧 Aprender algún framework UI de Javascript / back-end
  • 🟦 Comenzar a trabajar con modelos de IA y agentes
  • 🚀 Configurar entorno CI/CD
    • Trabajamos en rama develop
    • Cuando guardamos en GitHub rama main, se despliega automáticamente
  • 🌐 Configurar dominio
  • 🔒 Vigilar seguridad, mantenimiento, actualizaciones
    • Cloudflare, plan gratuito → 🟧 puede ser complejo, 🟥 bloqueos en España
  • 🐳 Aprender Docker
    • Simplifica configuración y despliegues en producción