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.
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:
.enven.gitignoreantes del primer commit.- Un
.env.exampleversionado con los nombres de las variables y valores de mentira. - Permisos
600en 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.