Monté un servidor nuevo desde cero para mis proyectos personales: un stack LEMP en Ubuntu 24.04, todo por SSH. Hay mil tutoriales de cómo instalar Linux, Nginx, MariaDB y PHP, y no voy a escribir el número mil uno. Lo que falta en casi todos es el porqué de cada decisión, el hardening de verdad y los detalles que rompen cuando ya creías que estaba todo hecho. Eso es lo que cuento aquí.
El «cómo instalar» lo encuentras en cualquier sitio. El criterio para no dejarlo a medias, no.
Por qué LEMP en 2026 (y por qué Ubuntu 24.04 LTS)
LEMP en vez de LAMP por una razón concreta: Nginx. Vengo de pelearme con Apache y sus .htaccess, y en mi último proyecto ya había movido todo a Nginx. La diferencia que me importa no es solo el rendimiento bajo carga; es que toda la configuración vive en un sitio, declarada explícitamente, en vez de repartida en archivos .htaccess que cualquiera puede dejar caer en una carpeta y cambiarte el comportamiento del servidor sin que te enteres. Más control, menos sorpresas.
Elegí Ubuntu 24.04 LTS (Noble Numbat) por lo aburrido que es, y lo digo como elogio. Una LTS me da cinco años de actualizaciones de seguridad sin tener que migrar de versión mayor. En un servidor que quiero olvidar y que funcione, esa estabilidad vale más que tener los paquetes más nuevos.
El stack de un vistazo: todo del repo, y por qué
Aquí va la primera decisión que un tutorial no suele justificar: lo dejé todo del repositorio por defecto de Ubuntu. Nginx, MariaDB 10.11 LTS y PHP 8.3-FPM, las versiones que trae 24.04, sin traer nada de upstream.
Es tentador añadir el repo de nginx.org para la mainline, o un PPA para la última PHP. No lo hice, y a propósito. Cuando dependes de los paquetes de Ubuntu, las actualizaciones de seguridad llegan por el canal oficial: un apt upgrade y listo, sin repos de terceros que mantener ni que se queden huérfanos. La contrapartida es no tener la versión más reciente de cada cosa. Para un servidor de producción que quiero estable, ese cambio me compensa siempre.
Para PHP instalé las extensiones que el stack necesita y nada más: imagick, xml, mbstring, curl, zip, intl y opcache. Cada extensión es código que corre con privilegios; cuantas menos haya, menos superficie.
Lo que un tutorial no te cuenta
La decisión técnica de la que más se habla mal por omisión es cómo conecta Nginx con PHP. La mayoría de tutoriales te dejan PHP-FPM escuchando en TCP (127.0.0.1:9000) y nadie explica por qué. Yo lo puse en socket Unix (unix:/var/run/php/php8.3-fpm.sock).
El motivo: en un servidor de una sola máquina, donde Nginx y PHP-FPM viven juntos, el socket Unix es más rápido. Se comunican directamente a través del sistema de archivos, sin pasar por la pila TCP/IP y su overhead. El TCP solo tiene sentido si PHP-FPM corre en una máquina separada de Nginx, que no es mi caso. Usar TCP en localhost es pagar un peaje de red para un viaje que no sale de casa.
El otro detalle es que un único server block sirve dos cosas a la vez: el portfolio en PHP plano en la raíz y un WordPress que vive en el subdirectorio /blog/. No necesito dos bloques ni dos dominios; con los location correctos, el mismo server reparte el tráfico. Aquí está el núcleo de la config, con las rutas anonimizadas:
server {
listen 443 ssl http2;
server_name example.com;
root /var/www/site/public;
index index.php index.html;
location / {
try_files $uri $uri/ /index.php?$args;
}
location /blog/ {
try_files $uri $uri/ /blog/index.php?$args;
}
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/var/run/php/php8.3-fpm.sock;
}
}
OPcache lo dejé activo desde el principio. Compila el bytecode de PHP una vez y lo cachea en memoria, así no se reparsea en cada petición. Para un sitio con tráfico real es de las mejoras de rendimiento más baratas que existes: cambias un par de líneas de config y ya está.
Endurecerlo desde el primer día
El hardening no es un paso final que haces cuando «tienes tiempo». Es lo primero, porque en cuanto el servidor tiene una IP pública empieza a recibir intentos de acceso automáticos. Los bots no esperan a que termines.
Empecé por la base de datos. mysql_secure_installation no es un trámite de pulsar Enter cinco veces: quité los usuarios anónimos, bloqueé el login remoto de root, eliminé la base de datos de prueba y puse una contraseña de root seria. Cada uno de esos pasos cierra una puerta que viene abierta por defecto.
El firewall lo gestioné con UFW, con una política de denegar todo y abrir solo lo imprescindible: SSH, HTTP y HTTPS. Nada más expuesto. Si un servicio no necesita estar accesible desde fuera, no lo está.
SSH lo moví a un puerto no estándar. Y aquí soy honesto sobre lo que esto es y lo que no es: no es seguridad real. Mover SSH del 22 no para a un atacante decidido. Lo que hace es reducir el ruido: la inmensa mayoría de los escaneos automáticos golpean el 22 y se van, así que cambiar el puerto me limpia los logs de basura y me deja ver los intentos que de verdad importan. El blindaje de SSH está en las claves y en deshabilitar el login por contraseña, no en el número de puerto.
El TLS lo resolví con Let’s Encrypt. Certificado gratis, renovación automática, HTTPS desde el día uno. En 2026 servir HTTP plano no tiene excusa, y montar esto cuesta diez minutos.
Usuarios y permisos: el lío que de verdad cuesta
Esta es la parte que ningún tutorial de «instala LEMP en cinco minutos» te cuenta, y es la que más tiempo me llevó. Tengo varios proyectos en el mismo servidor y quería que cada uno se gestionara por separado, sin que el acceso a uno diera acceso a los demás.
El esquema: un usuario SSH general para administrar la máquina, y luego un usuario FTP separado por cada proyecto. Cada uno entra solo a lo suyo.
El problema real aparece en cuanto mezclas eso con PHP. PHP-FPM corre como www-data, pero los archivos los sube un usuario FTP que es otra persona distinta. Si no alineas grupos y permisos, te pasa una de dos: o PHP no puede leer lo que el usuario FTP subió, o el usuario FTP no puede escribir donde PHP necesita. El servidor parece roto y la causa no salta a la vista, porque no es un error de configuración de Nginx ni de PHP: es de propiedad de archivos.
Lo resolví ajustando los grupos y los permisos de cada carpeta de proyecto para que el usuario FTP correspondiente y www-data pudieran convivir, cada uno escribiendo y leyendo lo que le toca, sin pisarse y sin que un proyecto tuviera acceso al de al lado. Suena simple escrito así; en la práctica es donde se va la tarde si no lo piensas antes.
Qué dejé listo para producción y qué haría distinto
Lo que tengo ahora es un servidor que puedo olvidar: actualizaciones de seguridad por el canal oficial, certificado que se renueva solo, firewall cerrado y cada proyecto en su corral. Para mis proyectos personales es exactamente el nivel que necesito.
Lo del socket Unix y OPcache lo repetiría sin pensarlo. Son decisiones de cero coste y beneficio inmediato que mucha gente se salta solo porque el tutorial que siguió las omitía.
¿Qué cambiaría? El acceso por FTP. Funciona, pero FTP es de otra época; un sftp sobre la misma conexión SSH sería más limpio y una cosa menos que endurecer por separado. No es urgente, pero es lo primero que tocaría en la próxima vuelta.
Si te llevas una idea de aquí, que sea esta: instalar el stack es la parte fácil y resuelta. El trabajo de verdad está en las decisiones que nadie copia y pega —el socket, los permisos, qué cierras y qué dejas abierto—. Ahí es donde un servidor pasa de «arranca» a «lo dejo en producción y duermo tranquilo».