Fase 5: Servidor VPS

Contenidos

  • ¿Qué es un servidor VPS?
  • Digital Ocean
  • Servidor web nginx
  • Servicio fail2ban
  • Firewall ufw

¿Qué es un servidor VPS?

  • 🐧 Se trata de un servidor Linux en la nube
    • Si sabes trabajar en terminal, sabes usar una VPS
  • 📚 Hay muchos detalles en Digital Ocean, sólo vamos a ver Droplets (VPS)
    • Hay que aprender y ser cuidadoso en seguridad
    • Aprende poco a poco, siempre entendiendo lo que haces
  • 💸 Precios

Crear Droplet en Digital Ocean

Contenidos

  • Crear droplet
  • Acceder vía SSH
  • Configurar servicios
  • Monitorizar estado y logs

Crear Droplet

  • Inferencia: Uso de modelos de Inteligencia Artificial en local (serverless)
  • Computación: El que nos interesa. Computación en la nube
    • Droplet: 💧 Crear un VPS ← El que nos interesa
    • GPU Droplet: Como los droplets pero con buenas tarjetas gráficas
    • Otros: Kubernetes, plataformas para Apps o funciones lambda
  • Data services: Sistemas de bases de datos en la nube
  • Insights: Monitorización de recursos y límites
  • Networking: 🧱 Firewall de red y otros como balanceador de carga, etc.
  • Storage: 🧊 Añadir volúmenes (almacenamiento)

Crear un Droplet

  • Elegir Datacenter: Disponibilidad regional

  • Elegir Sistema operativo:

    • 🏆 Debian: Estabilidad
    • Ubuntu: Facilidad (basado en Debian)
    • Fedora: Innovación y tecnología
    • CentOS: Empresarial
    • AlmaLinux / Rocky Linux: Reemplazo de CentOS

Crear Droplet con una App preinstalada

  • MarketPlace: Lista de apps preparadas para instalar
    • Docker → Droplet con Docker preinstalado listo para utilizar
    • OpenClaw → Droplet con OpenClaw preinstalado listo para usar
    • Coolify → Droplet con Coolify preinstalado listo para usar
    • n8n → Droplet con n8n preinstalado listo para usar
    • Wordpress → Droplet con Wordpress preinstalado listo para usar
    • Y muchas otras → Más de 250 apps

Características técnicas del Droplet

  • Procesador: Shared CPU / Dedicated CPU
  • Tipo: Basic / General / CPU-Optimized / Memory-Optimized
  • Disco: SSD / AMD NVMe SSD / Intel NVMe SSD

  • Dependiendo de la elección, te tocará un plan
  • Plan mínimo: 1vCPU + 512MB RAM + 10 GB SDD + 500GB Transfer = 4$/mes
  • Plan base: 1vCPU + 1GB RAM + 25 GB SSD + 1TB Transfer = 6$/mes
  • Y otros planes superiores...

Sistema de Backups

  • ActivarloPero Manz! yo no lo necesit...
  • Planes: Establece tu plan preferido
    • Weekly (Semanal) → +20% del precio del Droplet
    • Daily (Diario) → +30% del precio del Droplet
    • Usage-based (Personalizado) → Tu eliges cuando

  • Si no lo activas, luego no te quejes

Características internas del Droplet

  • Recomendado: Añadir una SSH Key (igual que lo hicimos con GitHub)
    • Hazlo siempre que puedas.
    • Si no, al menos, con Password (y que sea seguro)
  • Puedes activar IPv6 en tu servidor si lo deseas.
  • Puedes activar métricas y monitorización (recomendado)
  • Puedes utilizar un sistema de scripts de inicio.

Resumen final

  • Antes de crear el droplet, revisa bien el resumen.
  • 💸 Revisa el precio
  • Revisa que las características son las que necesitas.
  • 💸 Revisa el precio
  • Activa los backups si te olvidaste.
  • 💸 Revisa el precio

  • Aunque el precio te lo pone al mes, te cobran por horas.
  • Te cobran por tenerlo creado (¡aunque tengas el VPS apagado!).
  • 🔥 Truco: Si no estás usándolo, apágalo y conviértelo en Snapshot (más barato, $0.06/GB al mes)

Droplets reescalables

  • Los VPS en Digital Ocean (y en muchos otros) son elásticos.
  • Esto significa que puedes cambiarte de plan.
  • ⚠ ¡Acuerdate marcar la casilla Downscale!
    • Esto bloqueará y no escalará el almacenamiento.
    • Si amplias almacenamiento, luego no podrás bajar al plan inferior.

  • ⚠ Sin Downscale, el cambio de plan es irreversible.

¡Droplet creado! ¿Cómo accedemos?

  • Lo primero: localiza la IP del VPS
  • Abre una terminal
# Conecta a la IP con el usuario indicado
ssh usuario@IP

# En nuestro caso, la primera vez:
ssh root@111.22.33.44
...
Are you sure you want to continue connecting (yes/no/[fingerprint])?

# Este mensaje nos indica que conectamos por primera vez a una nueva máquina
  • Una vez accedido, tenemos que crear nuestro usuario (¡no trabajes como root!)
adduser manz            # Crear usuario (¡no usar useradd!)
passwd manz             # Ponerle un password (diferente a root, y seguro)
usermod -aG sudo manz   # Añadirle permisos de sudo
exit                    # Salir
ssh manz@111.22.33.44   # Comprobar si podemos acceder con nuevo usuario

sudo apt update                     # Comprobar si tenemos permisos de sudo
sudo apt install curl fish wget git # Instalar esenciales
chsh -s $(which fish)               # Poner fish por defecto
  • Opcionalmente, recuerda que puedes configurarlo a tu gusto...
# Instalar fisher + TIDE
curl -sL https://raw.githubusercontent.com/jorgebucaran/fisher/main/functions/fisher.fish | source
fisher install jorgebucaran/fisher
fisher install IlanCosman/tide@v6    # fish no está en su última versión. No importa
Configure tide prompt? [Y/n]         # Pulsa yes o luego escribe `tide configure`

# Sigue el asistente y finalmente pulsa y para guardar
# Vayan a Youtube y diganle al que se quejó que pulsar "y" no era algo tan difícil
  • Consejos de seguridad (muy recomendado) -ponerlo más tarde-
# Mantén siempre el VPS actualizado (sobre todo respecto a seguridad)
sudo apt update            # Actualizar la lista de paquetes
sudo apt upgrade           # Actualizar todo a su última versión (tardará un poco)

# Ahora que tienes acceso como usuario, desactiva el acceso de root desde fuera
sudo vim /etc/ssh/sshd_config
PermitRootLogin no         # Cambialo a "no"
sudo service sshd restart  # Guarda y reinicia el servidor ssh
# Podrás seguir accediendo con tu usuario, pero no como root (peligroso y ataques comunes)
sudo journalctl -u ssh | grep -i "Failed password for root"
sudo journalctl -u ssh | grep -i "Failed password for invalid user"

# Consejos:
# - Firewall de red (a nivel de CDN, Cloudflare)
# - Firewall de red (a nivel de datacenter, DO)
# - Firewall por software (ufw)
# - Firewall por software (fail2ban)

Nginx

Servidor web Nginx

  • Instalamos y configuramos Nginx
sudo apt install nginx
sudo nginx -v               # Nginx no lo puede usar un usuario normal
curl --head localhost:80    # Comprobamos si funciona el servidor
curl --head localhost:443   # No funciona, no tenemos HTTPS aún

Ahora puedes acceder desde la IP → http://111.22.33.44 → Sólo HTTP/80 no seguro (de momento)

  • Recientemente, aparecieron vulnerabilidades para Nginx: CVE-2026-42945, CVE-2026-40701, CVE-2026-42946 y CVE-2026-42934, todas corregidas en nginx 1.30.1 o superiores.
  • Debian no sube a últimas versiones, sino que aplica parches por separado
sudo apt-get changelog nginx | grep -i CVE-2026-42945

¡Utiliza un multiplexador!

sudo apt install tmux     # En el servidor VPS instala tmux
tmux                      # Lo abrimos (aparece una linea verde abajo)
ls -lh /etc               # Escribe un comando

ps aux | grep ssh         # En otra terminal, en tu PC busca la conexión ssh
manz       19542  0.1  0.0  20240  9824 pts/6
kill -9 19542             # Matamos el proceso (simulamos un corte de internet)

ssh manz@111.22.33.44     # Volvemos a conectar al VPS
tmux attach               # Recuperamos la sesión que se cortó
  • Alternativas a tmuxbyobuzellij.dev
    • bash (curl -L https://zellij.dev/launch | psub)
Acción tmux byobu
Divide la terminal (horizontal) CTRL+B + % CTRL+F3
Mueve a una división CTRL+B + / SHIFT+/
Redimensiona la división CTRL+B + CTRL+/ SHIFT+ALT+/
Divide la terminal (vertical) CTRL+B + " CTRL+F2
Por si te olvidas de los atajos CTRL+? F1

Configuración de nginx (web estática)

  • Estructura de carpetas de /etc/nginx
📁 /etc/nginx                     # Carpeta de configuración
├── 📁 modules-available          # Módulos disponibles
├── 📁 modules-enabled            # Módulos activados
├── 📁 conf.d
├── 📁 sites-available            # Sitios (dominios/config) disponibles
│   └── 📄 default
├── 📁 sites-enabled              # Sitios (dominios/config) activados
│   └── 📄 default -> /etc/nginx/sites-available/default
├── 📁 snippets                   # Configuraciones compartidas
├── ⚙ fastcgi.conf
├── 📄 fastcgi_params
├── 📄 mime.types                 # MIMES
└── ⚙ nginx.conf                 # Configuración general
  • Configuración general de nginx.conf. Habitualmente no se toca.
user www-data;
worker_processes auto;
worker_cpu_affinity auto;
pid /run/nginx.pid;
error_log /var/log/nginx/error.log;
include /etc/nginx/modules-enabled/*.conf;

events {
  worker_connections 768;
}
http {
  # ...
  server_tokens off;                      # Desactivar info

  include /etc/nginx/mime.types;          # MIME
  default_type application/octet-stream;

  ssl_protocols TLSv1.2 TLSv1.3;
  ssl_prefer_server_ciphers off;

  access_log /var/log/nginx/access.log;   # Default log
  gzip on;                                # Compresion

  include /etc/nginx/conf.d/*.conf;       # Config
  include /etc/nginx/sites-enabled/*;     # Sites
}
  • Vamos a crear un nuevo fichero de configuración (organizar)
# Muestrame sólo las lineas no comentadas
# (No tienes permisos para guardar)
grep -v '^\s*#' default > dominio.com.conf

# Con tee guardas en el fichero la salida
grep -v '^\s*#' default | sudo tee dominio.com.conf

# Anulas la salida visual, por si te da TOC
grep -v '^\s*#' default | sudo tee dominio.com.conf > /dev/null

# Eliminamos config default de sitios ACTIVOS
sudo rm /etc/nginx/sites-enabled/default

# Editamos la config
sudo nano /etc/nginx/sites-available/dominio.com.conf
  • Fichero dominio.com.conf
server {
  listen 80 default_server;
  listen [::]:80 default_server;

  root /var/www/html;
  index index.html index.htm index.nginx-debian.html;
  server_name _;

  location / {
    try_files $uri $uri/ =404;
  }
}
  • Con listen nginx escucha en el puerto indicado
    • default_serverfallback a IP/dominio mal configurado
    • Con [::]: escuchamos en IPv6 (coméntalo si no usas IPv6)
  • Con root indicas la carpeta donde tienes la web
  • Con index indicas los nombres que buscas por defecto
    • Lo cambiamos a index index.html
  • Con server_name _ indicamos que es un servidor catch-all
    • server_name dominio.com www.dominio.com
  • Con location establecemos reglas
    • Entras a / → prueba $uri (file), $uri/ (folder), sino 404.
  • Ruta de la web /var/www/
# server {
#   root /var/www/dominio.com;
# }

sudo chown www-data:adm /var/www
sudo chmod -R 775 /var/www
sudo usermod -aG manz adm
  • Config: Separamos por dominios/subdominios
  • Cambiamos el autor (a veces viene como root)
  • Damos permiso a la carpeta /var/www
  • Somos usuarios con permisos para guardar archivos
# Abrimos nueva sesión (para que lea nuevos permisos)
mkdir -p /var/www/dominio.com
nano /var/www/dominio.com/index.html

sudo nginx -t
sudo service nginx reload
  • Creamos /var/www/dominio.com
  • Creamos un index.html con un <h1>Hola mundo!</h1>
  • Comprobamos que no haya fallos de sintaxis
  • Reiniciamos nginx para que cargue la info

Monitorización en nginx

  • Por defecto, los registros son globales. Vamos a dividirlos en dominio.com/*.log
server {
  # ...
  access_log /var/log/nginx/dominio.com/access.log;
  error_log /var/log/nginx/dominio.com/error.log;
}
  • Podemos configurar dos ficheros de logs
    • access.log con los accesos de usuarios
    • error.log con los errores de acceso
    • Ambos en dominio.com/, hay que crearlo
sudo mkdir -p /var/log/nginx/dominio.com
sudo chown www-data:adm /var/log/nginx/dominio.com
sudo chmod 755 /var/log/nginx/dominio.com

# Creamos
sudo ln -s /etc/nginx/sites-available/dominio.com.conf \
           /etc/nginx/sites-enabled/dominio.com.conf
sudo nginx -t   # Comprueba si hay error de sintaxis
sudo service nginx reload
  • Creamos carpeta, le damos permisos
  • Apuntamos al dominio nuevo en sites-enabledpara vagos
  • Reiniciamos nginx para que cargue la config
  • Registrar el access.log
sudo apt install ccze

# Mostrar log
cat /var/log/nginx/dominio.com/access.log

# Mostrar log en tiempo real
tail -f /var/log/nginx/dominio.com/access.log

# Veamos a crear un script (para proteger privacidad xD)
sudo vim /usr/local/bin/log
#!/bin/bash

LOGFILE=${1:-/var/log/nginx/dominio.com/access.log}
sed -E 's/([0-9]+\.[0-9]+\.[0-9]+)\.[0-9]+/\1.x/g' $LOGFILE | ccze -A

sudo chmod +x /usr/local/bin/log  # Damos permisos
log | less -R    # Ejecutamos, paginamos (soporte ANSI)
  • Automatizar y gestionar logs → /etc/logrotate.d/nginx
# Configuración base
/var/log/nginx/*.log {
  daily                  # Rota diariamente
  missingok              # Si no encuentra el log, skip
  rotate 14              # Conserva máx. 14 logs
  compress               # Comprime los logs rotados
  delaycompress          # La primera rotación no se comprime
  notifempty             # No rota si el log está vacío
  create 0640 www-data adm
  sharedscripts
  prerotate # No tocar (hasta endscript)
  postrotate # No tocar (hasta endscript)
}
# Configuración optimizada
/var/log/nginx/*/*.log {  # Pillar subdominios
  weekly                  # Rota semanalmente
  missingok
  rotate 14
  minsize 1M              # No rota si no tiene 1MB
  # size 50M              # Rota en cuanto llega a 50MB
  compress
  delaycompress
  notifempty
  create 0640 www-data adm
  sharedscripts
  prerotate # No tocar (hasta endscript)
  postrotate # No tocar (hasta endscript)
}
  • Revisar los access.log más cómodamente
# LNAV: Log Viewer for terminal
sudo apt install lnav
lnav /var/log/nginx/dominio/access.log      # F2 / Revisa TIMELINE

# GoAccess: Powerful Log Viewer
sudo apt install goaccess
goaccess /var/log/nginx/dominio/access.log      # Marcamos primera + ENTER
sudo vim /etc/goaccess/goaccess.conf            # Descomentamos :13 :36 y :80
goaccess /var/log/nginx/dominio.com/access.log -o html >/var/www/dominio.com/report.html

Configurar HTTPS

  • El certificado se puede añadir con Let's encrypt, aquí lo haremos autofirmado de prueba
sudo apt install openssl        # Suele estar ya instalado
sudo mkdir -p /etc/nginx/ssl    # Carpeta de certificados

# ⚠⚠⚠ ↓ Generamos certificado AUTOFIRMADO (es de prueba)
sudo openssl req -x509 -nodes -days 365 \
  -newkey rsa:2048 \
  -keyout /etc/nginx/ssl/dominio.com.key \
  -out /etc/nginx/ssl/dominio.com.crt

Country Name → ES / State → Spain
Organization → Cocoliso Ltd
Common Name → 111.22.33.44

# Opción 1 (fácil): Cloudflare HTTPS → HTTP (plan gratis)
# Opción 2 (media): Let's Encrypt (gratuito)
# Opción 3 (cara): Comprar certificado (pago)
server {
  listen 443 ssl default_server;         # 80 → 443
  listen [::]:443 ssl default_server;    # → ssl

  ssl_certificate       /etc/nginx/ssl/dominio.com.crt;
  ssl_certificate_key   /etc/nginx/ssl/dominio.com.key;
  ssl_protocols TLSv1.2 TLSv1.3;
  ssl_prefer_server_ciphers on;
  # ...
}

server {
  listen 80 default_server;
  server_name _;
  return 301 https://$host$request_uri;
}
# Requisitos:
# - Dominio apuntando a nuestro servidor
# - Puerto 80 (HTTP) abierto y accesible con nginx con server_name
# - Necesitaremos acceso root (o sudo)

# Instalamos el bot de certificados y el complemento para nginx
sudo apt install certbot python3-certbot-nginx

# Obtenemos el certificado (automático)
sudo certbot --nginx -d dominio.com -d www.dominio.com

# Verificamos (se crean en /etc/letsencrypt/live/)
sudo certbot certificates

# Renovaciones (Debian 13 ya lo añade en systemd timer)
systemctl list-timers | grep certbot
  • Optimizar Nginx añadiendo HTTP/2 que vimos en la clase de Rendimiento
  • Para activar HTTP/2 es necesario tener HTTPS
server {
  listen 443 ssl default_server;
  listen [::]:443 ssl default_server;
  http2 on;                                  # Añadimos HTTP/2

  # ...

  ssl_prefer_server_ciphers off;             # Cliente elige cifrado
  ssl_session_cache shared:SSL:10m;          # Sesiones TLS reusables
  ssl_session_timeout 10m;                   # Idem

  # ...
}

Usando location en nginx

Código Descripción
location /api Prefijo. Habitual.
location = /api/ Estricto. Muy rápido.

RegEx (incluye ~)

Código Descripción
location ~ \.(css|js)$ RegEx
~* en lugar de ~ RegEx (ignora min/may)
^~ en lugar de ~ RegEx de alta prioridad
(no procesa otras regex)
server {
  location /api {
    allow 84.84.84.84;     # Permite acceso solo a IP 84.84.84.84
    deny all;              # Deniega al resto
  }

  # Mejora el cache de estáticos
  location ~* \.(css|js|jpe?g|a?png|avif|webp|jxl|gif|ico|svg)$ {
    expires 7d;
    add_header Cache-Control "public, max-age=604800";
  }

  # Deniega archivos ocultos: .git, .env, .htaccess, ...
  location ~ /\.(.*) {
    deny all;
  }
}

Bloqueos o rate limits

# /etc/nginx/nginx.conf
http {
  limit_req_zone $binary_remote_addr zone=api:10m rate=5r/s;
  # ...
}

# /etc/nginx/sites-available/dominio.com.conf
server {
  location /api/ {
    limit_req zone=api burst=5 nodelay;
  }
}
  • Limitar accesos a /api/ en un tiempo concreto
  • Bucket (zona) api de 10MB → Permite 5 request/seg
  • Burst de 5 peticiones extra → sin delay
# Desde nuestro equipo
ab -n 100 -c 20 https://111.22.33.44/api/
Complete requests:      100
Failed requests:        0

# Desde servidor VPS
sudo nginx -t
sudo service nginx restart

# Desde nuestro equipo
ab -n 100 -c 20 https://111.22.33.44/api/
Complete requests:      100
Failed requests:        88
  • El comando ab lanza peticiones al servidor
  • Lo probamos antes y después de meter el rate limiting

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 22/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

Pasos futuros

  • 🚀 Configurar entorno CI/CD
    • 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

Preguntas