← Tu laboratorio en cualquier parteSerie Remoto · paso 1 de 11

Tu propio servidor: VPS alquilado o máquina en casa, y cómo dejarlo decente en una tarde

Olmillos Server aloja esta web, varias aplicaciones propias, el panel del laboratorio y los agentes remotos. Así se monta uno desde cero: qué comprar o alquilar, cómo cerrarlo y cómo organizar las apps para no perderte.

Dificultadfácil
Tiempouna tarde
IngredientesUbuntu Server 22.04/24.04 · SSH con claves · ufw · Docker + Compose · Nginx · Certbot

Todo lo que se cuenta en esta serie cuelga de una máquina que en el laboratorio llamamos Olmillos Server. No es un rack ni un servicio en la nube: es un servidor Ubuntu corriente que sirve esta web, varias aplicaciones propias en producción, el panel desde el que controlamos los modelos y los puentes que nos dejan trabajar con agentes de IA desde el móvil.

Esta primera guía va de lo aburrido que hace que todo lo demás funcione: tener un servidor, cerrarlo bien y ordenar lo que vive dentro.

VPS o máquina en casa: la decisión de verdad

VPS alquilado Máquina en casa
Coste de entrada 4–10 €/mes para empezar lo que cueste la máquina
IP pública fija sí, incluida casi nunca; necesitas DNS dinámico
Si se va la luz no es tu problema es tu problema
Disco y RAM caros de ampliar baratos de ampliar
Llegar a tu LAN solo por VPN directo
Latencia a tus GPUs la de internet la de tu switch

Nuestra elección: una máquina en casa, porque el servidor tiene que hablar con dos GX10 y un portátil que están en la misma red, y porque dos terabytes en un VPS cuestan al año lo que el disco. Si tu caso es «quiero una web y dos apps», empieza por un VPS: te ahorras el router, el DNS dinámico y los cortes de luz.

Lo que viene a continuación vale igual para los dos.

Paso 1 · Usuario, claves y nada de contraseñas por SSH

En un VPS recién creado entras como root. Lo primero es no volver a hacerlo:

adduser tu_usuario
usermod -aG sudo tu_usuario

Desde tu ordenador, copia tu clave pública (si no tienes, ssh-keygen -t ed25519):

ssh-copy-id tu_usuario@IP_DEL_SERVIDOR

Comprueba que entras sin contraseña antes de cerrar la puerta. Después, en el servidor, edita /etc/ssh/sshd_config.d/10-endurecido.conf:

PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
sudo sshd -t && sudo systemctl reload ssh

Deja siempre una sesión abierta mientras cambias SSH. Si la configuración nueva te deja fuera, la sesión vieja sigue viva y puedes deshacerlo.

Paso 2 · Cortafuegos: solo lo que tiene que entrar

sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow OpenSSH
sudo ufw allow 80,443/tcp
sudo ufw enable

Y ya. Las aplicaciones no se abren al exterior una por una: escuchan en 127.0.0.1 y las publica Nginx (paso 5). En la guía 2 cerramos incluso el SSH público.

Dos añadidos que no cuestan nada:

sudo apt install unattended-upgrades fail2ban
sudo dpkg-reconfigure -plow unattended-upgrades

Paso 3 · Docker, y una carpeta por aplicación

Cada aplicación de Olmillos Server vive en su propia carpeta dentro de ~/apps/, con su docker-compose.yml y su .env, y publica su puerto solo en loopback. Por ejemplo, una app típica es un contenedor con la API y la web en 127.0.0.1:8130 y una base de datos PostgreSQL en su propio contenedor:

~/apps/
├── mi-app/           docker-compose.yml · .env · dist/ · server/   → 127.0.0.1:8130
├── otra-app/         …                                             → 127.0.0.1:81xx
└── …
# docker-compose.yml — el detalle importante es el 127.0.0.1
services:
  app:
    build: .
    env_file: .env
    ports:
      - "127.0.0.1:8130:3000"
    restart: unless-stopped
  db:
    image: postgres:16
    env_file: .env
    volumes:
      - db:/var/lib/postgresql/data
    restart: unless-stopped
volumes:
  db:

Sin ese 127.0.0.1:, Docker publica el puerto en todas las interfaces y se salta ufw, porque escribe sus propias reglas de iptables. Es el error más común de un servidor casero.

Un consejo que te ahorrará un disgusto: apunta en un fichero qué puerto usa cada app. Parece burocracia hasta el día que dos apps quieren el 3000.

Paso 4 · Secretos fuera de Git, siempre

El .env de cada app contiene contraseñas de base de datos y claves de sesión. Tres reglas que aplicamos en todos los proyectos:

  1. .env en .gitignore antes del primer commit.
  2. Un .env.example versionado con los nombres de las variables y valores de mentira.
  3. Permisos 600 en el servidor: chmod 600 ~/apps/*/.env.

Y una cuarta, aprendida a golpes: si una clave se imprime en pantalla durante una sesión con un agente de IA, se rota. No «seguramente no pasa nada»: se rota. Tampoco la pases como argumento de un proceso, que cualquiera con ps la puede leer (P-011).

Paso 5 · Nginx delante de todo, con HTTPS

Nginx recibe el tráfico público en el 443 y lo reparte a cada app en loopback:

server {
    listen 443 ssl http2;
    server_name recursos.tudominio.es;

    ssl_certificate     /etc/letsencrypt/live/recursos.tudominio.es/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/recursos.tudominio.es/privkey.pem;

    location / {
        proxy_pass http://127.0.0.1:8130;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}

El certificado, con Certbot en modo webroot para que la renovación no pare Nginx:

sudo certbot certonly --webroot -w /var/www/html -d recursos.tudominio.es
sudo certbot renew --dry-run

Añade un deploy hook que valide y recargue Nginx tras cada renovación; si no, el certificado nuevo no se usa hasta el siguiente reinicio.

Si usas un panel (aaPanel, CloudPanel…)

Olmillos Server lleva aaPanel, que instala su propio Nginx en otra ruta. Eso tiene una trampa que nos costó un rato: nginx -t a secas valida el Nginx del sistema, no el que está sirviendo (P-043). Valida siempre con el binario y la configuración del panel:

/www/server/nginx/sbin/nginx -t -c /www/server/nginx/conf/nginx.conf

Paso 6 · Si está en casa: DNS dinámico

Una máquina en casa no tiene IP fija. La solución que usamos es un script de pocas líneas que pregunta cuál es la IP pública y actualiza los registros de Cloudflare por API, lanzado por cron cada cinco minutos:

# /etc/crontab
*/5 * * * * root /usr/local/bin/cf-ddns.sh >> /var/log/cf-ddns.log 2>&1

El token de Cloudflare que usa ese script solo necesita permiso de editar DNS de esa zona. Nada más. Cuando añadas un subdominio nuevo, acuérdate de añadirlo también a la lista del script (nos pasó: el registro existía, el script no lo conocía y la IP se quedaba vieja).

Paso 7 · Copias: el servidor también es tu almacén

Además de servir cosas, Olmillos Server guarda la copia completa del espacio de trabajo. Desde el portátil, un script empaqueta todos los proyectos —incluidas bases de datos locales y configuración ignorada por Git, excluyendo node_modules, dist y cachés— y lo sube con un manifiesto de qué commit tenía cada repositorio:

el-nido-ia-lab-20260927-101500.tar.gz
el-nido-ia-lab-20260927-101500.manifest.txt   ← HEAD y cambios pendientes de cada repo

GitHub guarda el historial del código; el servidor guarda el estado entero, lo que Git no ve. Son cosas distintas y conviene tener las dos.

Comprobación final

sudo ufw status verbose                 # solo 22, 80 y 443
sudo ss -ltnp | grep -v 127.0.0.1       # nada inesperado escuchando fuera
docker ps --format '{{.Names}}\t{{.Ports}}'   # todos los puertos en 127.0.0.1
sudo certbot certificates               # fechas de caducidad

Si la segunda orden enseña algo que no esperabas, ese es tu siguiente problema de seguridad.

Siguiente: la red privada que lo une todo, y por qué a partir de ahí el SSH ya no necesita estar en internet.