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
Aapuntando a la IP pública del servidor, necesario para el paso de HTTPS. En la guía se usayour_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 aindex.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 snippetfastcgi-php.confde Ubuntu ya defineSCRIPT_FILENAMEy comprueba que el archivo existe, así que no hace falta repetirlo.location ~ /\.(?!well-known)bloquea archivos ocultos como.envo.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
Notasi antes descomentaste
gzip_typesogzip_varyen/etc/nginx/nginx.conf,nginx -tavisará de una directiva duplicada. En ese caso quita esas líneas de uno de los dos archivos.
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 defastcgi_passcoincide 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 enrooto faltainclude snippets/fastcgi-php.confen el bloque de PHP. Revisa la ruta derooten el server block.- El navegador descarga el archivo PHP: la petición no está entrando en
location ~ \.php$. Comprueba consudo nginx -T | grep -n fastcgi_passque el bloque está en el server block activo. 413 Request Entity Too Large: el archivo superaclient_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 consudo ss -ltnp | grep ':80'y elimínalo consudo 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.
