LEMP es la variante de LAMP que sustituye Apache por Nginx: Linux, Nginx (pronunciado "engine-x", de ahí la E), MySQL y PHP. Nginx no ejecuta PHP por sí mismo, sino que pasa cada petición .php a PHP-FPM, un servicio independiente que mantiene un grupo de procesos PHP. En este tutorial instalarás Nginx, MySQL 8.0 y PHP 8.3 con PHP-FPM en Ubuntu 24.04, configurarás un server block para tu dominio, comprobarás que PHP lee datos de MySQL y activarás HTTPS con Let's Encrypt.

Requisitos previos

  • Un servidor con Ubuntu 24.04 LTS, por ejemplo un VPS de CubePath, con al menos 1 GB de RAM.
  • Un usuario no root con privilegios sudo.
  • Un dominio con un registro A apuntando a la IP pública del servidor, necesario para el paso de HTTPS. En la guía se usa your_domain: sustitúyelo por el tuyo.

Paso 1: Instalar Nginx y abrir el firewall

Actualiza el índice de paquetes e instala Nginx:

sudo apt update
sudo apt install -y nginx

El servicio queda arrancado y habilitado al instalarlo. Compruébalo:

sudo systemctl status nginx --no-pager
● nginx.service - A high performance web server and a reverse proxy server
     Loaded: loaded (/usr/lib/systemd/system/nginx.service; enabled; preset: enabled)
     Active: active (running) since Thu 2026-09-24 11:02:41 UTC; 10s ago

Permite SSH en UFW antes de activarlo (si no, perderás la conexión) y después el perfil Nginx Full, que abre los puertos 80 y 443:

sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
sudo ufw enable
sudo ufw status
Status: active

To                         Action      From
--                         ------      ----
OpenSSH                    ALLOW       Anywhere
Nginx Full                 ALLOW       Anywhere
OpenSSH (v6)               ALLOW       Anywhere (v6)
Nginx Full (v6)            ALLOW       Anywhere (v6)

Abre http://your_server_ip en el navegador: deberías ver la página "Welcome to nginx!". Desde la terminal:

curl -sI http://localhost | head -n 1
HTTP/1.1 200 OK

Paso 2: Instalar MySQL

Instala el servidor MySQL:

sudo apt install -y mysql-server

Comprueba que está en marcha:

sudo systemctl status mysql --no-pager
mysql --version
mysql  Ver 8.0.43-0ubuntu0.24.04.1 for Linux on x86_64 ((Ubuntu))

Ejecuta el asistente de seguridad para eliminar los usuarios anónimos, la base de datos de prueba y el acceso remoto de root:

sudo mysql_secure_installation

Responde Y a todas las preguntas. En Ubuntu, el usuario root de MySQL usa el plugin auth_socket: solo puede entrar el usuario root del sistema, sin contraseña. No lo cambies a autenticación por contraseña; es más seguro así y las aplicaciones usarán su propio usuario.

Comprueba el acceso con sudo mysql y sal con exit.

Paso 3: Instalar PHP-FPM

Instala PHP-FPM, el conector de MySQL y las extensiones habituales. Instala php-fpm y no el metapaquete php: este último arrastra Apache como dependencia, y Apache intentaría ocupar el puerto 80 que ya usa Nginx.

sudo apt install -y php-fpm php-mysql php-curl php-gd php-mbstring php-xml php-zip php-intl

Comprueba la versión y el servicio:

php -v
sudo systemctl status php8.3-fpm --no-pager
PHP 8.3.6 (cli) (built: Jul 14 2025 18:30:55) (NTS)
...
● php8.3-fpm.service - The PHP 8.3 FastCGI Process Manager
     Active: active (running) since Thu 2026-09-24 11:05:12 UTC; 20s ago

PHP-FPM escucha en un socket Unix, que es la dirección a la que Nginx enviará las peticiones PHP:

ls -l /run/php/php8.3-fpm.sock
srw-rw---- 1 www-data www-data 0 Sep 24 11:05 /run/php/php8.3-fpm.sock

Paso 4: Crear un server block para tu dominio

En Nginx, cada sitio se define en un server block (el equivalente a un virtual host de Apache). Crea el directorio del sitio y asígnalo a tu usuario:

sudo mkdir -p /var/www/your_domain
sudo chown -R $USER:$USER /var/www/your_domain

Crea el archivo de configuración:

sudo nano /etc/nginx/sites-available/your_domain
server {
    listen 80;
    listen [::]:80;
    server_name your_domain www.your_domain;

    root /var/www/your_domain;
    index index.php index.html;

    access_log /var/log/nginx/your_domain.access.log;
    error_log /var/log/nginx/your_domain.error.log;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location ~ \.php$ {
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:/run/php/php8.3-fpm.sock;
    }

    location ~ /\.(?!well-known) {
        deny all;
    }

    add_header X-Content-Type-Options "nosniff" always;
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;
}

Qué hace cada bloque:

  • location / sirve el archivo o directorio pedido y, si no existe, pasa la petición a index.php. Es lo que esperan WordPress, Laravel y la mayoría de aplicaciones PHP con URL amigables.
  • location ~ \.php$ envía los archivos PHP a PHP-FPM. El snippet fastcgi-php.conf de Ubuntu ya define SCRIPT_FILENAME y comprueba que el archivo existe, así que no hace falta repetirlo.
  • location ~ /\.(?!well-known) bloquea archivos ocultos como .env o .git, dejando pasar /.well-known/, que usa Let's Encrypt.

Habilita el sitio creando un enlace en sites-enabled, desactiva el sitio por defecto y comprueba la sintaxis:

sudo ln -s /etc/nginx/sites-available/your_domain /etc/nginx/sites-enabled/
sudo unlink /etc/nginx/sites-enabled/default
sudo nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

Recarga Nginx:

sudo systemctl reload nginx

Paso 5: Crear una base de datos y un usuario

Entra en MySQL:

sudo mysql

Crea una base de datos, un usuario con permisos solo sobre ella y una tabla de ejemplo. Sustituye your_strong_password por una contraseña robusta:

CREATE DATABASE app_db CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;
CREATE USER 'app_user'@'localhost' IDENTIFIED BY 'your_strong_password';
GRANT ALL PRIVILEGES ON app_db.* TO 'app_user'@'localhost';
CREATE TABLE app_db.tareas (id INT AUTO_INCREMENT PRIMARY KEY, titulo VARCHAR(255) NOT NULL);
INSERT INTO app_db.tareas (titulo) VALUES ('Instalar Nginx'), ('Instalar MySQL'), ('Instalar PHP-FPM');
exit

El usuario se crea con el plugin caching_sha2_password, el predeterminado de MySQL 8, que la extensión mysqlnd de PHP 8.3 soporta sin configuración adicional. Comprueba que puede leer la tabla:

mysql -u app_user -p -e "SELECT * FROM app_db.tareas;"
+----+------------------+
| id | titulo           |
+----+------------------+
|  1 | Instalar Nginx   |
|  2 | Instalar MySQL   |
|  3 | Instalar PHP-FPM |
+----+------------------+

Paso 6: Probar PHP y la conexión a MySQL

Crea una página PHP que se conecte con PDO y liste las filas:

nano /var/www/your_domain/prueba.php
<?php
$pdo = new PDO(
    'mysql:host=localhost;dbname=app_db;charset=utf8mb4',
    'app_user',
    'your_strong_password',
    [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
);

echo '<h2>Tareas</h2><ul>';
foreach ($pdo->query('SELECT titulo FROM tareas') as $fila) {
    echo '<li>' . htmlspecialchars($fila['titulo']) . '</li>';
}
echo '</ul><p>PHP ' . PHP_VERSION . ' (' . PHP_SAPI . ')</p>';

Pídela desde la terminal o abre http://your_domain/prueba.php en el navegador:

curl -s -H "Host: your_domain" http://localhost/prueba.php
<h2>Tareas</h2><ul><li>Instalar Nginx</li><li>Instalar MySQL</li><li>Instalar PHP-FPM</li></ul><p>PHP 8.3.6 (fpm-fcgi)</p>

fpm-fcgi confirma que la página la ha ejecutado PHP-FPM. Borra el archivo, porque contiene la contraseña de la base de datos:

rm /var/www/your_domain/prueba.php

Paso 7: Ajustar PHP-FPM y Nginx

Límites de PHP

PHP-FPM tiene su propio php.ini, distinto del de la línea de comandos. Edítalo:

sudo nano /etc/php/8.3/fpm/php.ini

Busca estas directivas (con Ctrl+W en nano) y ajústalas:

upload_max_filesize = 64M
post_max_size = 64M
memory_limit = 256M
expose_php = Off
date.timezone = Europe/Madrid

Tamaño del grupo de procesos

El pool www define cuántos procesos PHP puede lanzar PHP-FPM. El valor que más importa es pm.max_children: si es demasiado alto, el servidor se queda sin memoria bajo carga; si es demasiado bajo, las peticiones esperan en cola. Con la aplicación funcionando y algo de tráfico, mide la memoria media por proceso:

ps --no-headers -o rss -C php-fpm8.3 | awk '{ sum += $1; n++ } END { printf "%.0f MB por proceso\n", sum / n / 1024 }'
38 MB por proceso

Divide la memoria que quieras dedicar a PHP entre ese valor. Por ejemplo, en un servidor de 2 GB en el que reservas 1 GB para PHP: 1024 / 38 ≈ 26 procesos. Edita el pool:

sudo nano /etc/php/8.3/fpm/pool.d/www.conf
pm = dynamic
pm.max_children = 25
pm.start_servers = 4
pm.min_spare_servers = 2
pm.max_spare_servers = 6
pm.max_requests = 500

pm.max_requests recicla cada proceso tras 500 peticiones, lo que evita que pequeñas fugas de memoria de la aplicación se acumulen. Comprueba la configuración y reinicia PHP-FPM:

sudo php-fpm8.3 -t
sudo systemctl restart php8.3-fpm
NOTICE: configuration file /etc/php/8.3/fpm/php-fpm.conf test is successful

Límites de Nginx

Nginx rechaza por defecto los cuerpos de petición de más de 1 MB, lo que haría fallar las subidas aunque PHP permita 64 MB. Añade un archivo en conf.d, que Nginx incluye dentro del bloque http:

sudo nano /etc/nginx/conf.d/tuning.conf
server_tokens off;
client_max_body_size 64M;
gzip_vary on;
gzip_types text/plain text/css application/javascript application/json image/svg+xml application/xml;

server_tokens off oculta la versión de Nginx y gzip_types amplía la compresión (que Ubuntu ya activa para HTML con gzip on) al resto de recursos de texto. Comprueba y recarga:

sudo nginx -t
sudo systemctl reload nginx

Paso 8: Activar HTTPS con Let's Encrypt

Instala Certbot con su plugin de Nginx:

sudo apt install -y certbot python3-certbot-nginx

Comprueba que el dominio resuelve a tu servidor y solicita el certificado:

dig +short your_domain
sudo certbot --nginx -d your_domain -d www.your_domain

Certbot te pedirá un correo para avisos y que aceptes los términos. Después añade al server block la escucha en el puerto 443 con el certificado y una redirección de HTTP a HTTPS:

Successfully received certificate.
Certificate is saved at: /etc/letsencrypt/live/your_domain/fullchain.pem
...
Congratulations! You have successfully enabled HTTPS on https://your_domain and https://www.your_domain

El paquete instala un temporizador de systemd que renueva los certificados antes de que caduquen. Compruébalo y simula una renovación:

systemctl list-timers certbot.timer
sudo certbot renew --dry-run

Verifica el acceso por HTTPS y que las cabeceras ya no revelan versiones:

curl -sI https://your_domain | grep -iE '^(HTTP|server)'
HTTP/1.1 200 OK
Server: nginx

Paso 9: Programar copias de seguridad de la base de datos

Crea el directorio de destino y un script que haga la copia y borre las de más de siete días:

sudo mkdir -p /var/backups/mysql
sudo nano /usr/local/bin/mysql-backup.sh
#!/usr/bin/env bash
set -euo pipefail

BACKUP_DIR="/var/backups/mysql"
DB_NAME="app_db"
RETENTION_DAYS=7
TIMESTAMP="$(date +%Y%m%d-%H%M%S)"

mysqldump --single-transaction --routines --triggers "$DB_NAME" \
  | gzip > "$BACKUP_DIR/${DB_NAME}-${TIMESTAMP}.sql.gz"

find "$BACKUP_DIR" -name "${DB_NAME}-*.sql.gz" -mtime +"$RETENTION_DAYS" -delete

Como se ejecuta como root, mysqldump entra por auth_socket sin contraseñas en el script. Dale permisos, pruébalo y prográmalo cada noche a las 3:00:

sudo chmod 750 /usr/local/bin/mysql-backup.sh
sudo /usr/local/bin/mysql-backup.sh
ls -lh /var/backups/mysql
echo '0 3 * * * root /usr/local/bin/mysql-backup.sh' | sudo tee /etc/cron.d/mysql-backup

Para restaurar una copia: gunzip < archivo.sql.gz | sudo mysql app_db. Guarda también copias fuera del servidor.

Solución de problemas

  • 502 Bad Gateway: Nginx no puede hablar con PHP-FPM. Comprueba que el servicio está activo (sudo systemctl status php8.3-fpm) y que la ruta de fastcgi_pass coincide con el socket real (ls /run/php/). El motivo exacto aparece en /var/log/nginx/your_domain.error.log.
  • File not found. o página en blanco al pedir un .php: el archivo no existe en root o falta include snippets/fastcgi-php.conf en el bloque de PHP. Revisa la ruta de root en el server block.
  • El navegador descarga el archivo PHP: la petición no está entrando en location ~ \.php$. Comprueba con sudo nginx -T | grep -n fastcgi_pass que el bloque está en el server block activo.
  • 413 Request Entity Too Large: el archivo supera client_max_body_size. Revisa el paso 7 y recarga Nginx.
  • Puerto 80 ocupado al arrancar Nginx: probablemente se instaló Apache con el metapaquete php. Compruébalo con sudo ss -ltnp | grep ':80' y elimínalo con sudo apt purge apache2 libapache2-mod-php8.3.

Conclusión

Tienes un stack LEMP funcionando en Ubuntu 24.04: Nginx sirve tu dominio por HTTPS con renovación automática, PHP-FPM ejecuta PHP 8.3 con un pool dimensionado para tu memoria y MySQL guarda los datos con un usuario propio y copias diarias. A partir de aquí puedes desplegar WordPress o Laravel en /var/www/your_domain, añadir Redis como caché de objetos y sesiones, o activar la caché FastCGI de Nginx para servir páginas sin pasar por PHP.