--- title: "Cómo instalar un stack LEMP en Ubuntu 24.04 con PHP-FPM" description: "Instala Nginx, MySQL 8 y PHP 8.3 con PHP-FPM en Ubuntu 24.04, configura un server block, prueba la conexión a la base de datos y activa HTTPS con Certbot." category: "Despliegue de Aplicaciones" language: es source: https://cubepath.com/es/docs/despliegue-de-aplicaciones/instalacion-de-stack-lemp-linux-nginx-mysql-php updated: 2026-09-25 --- # Cómo instalar un stack LEMP en Ubuntu 24.04 con PHP-FPM 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: ```bash sudo apt update sudo apt install -y nginx ``` El servicio queda arrancado y habilitado al instalarlo. Compruébalo: ```bash sudo systemctl status nginx --no-pager ``` ```output ● 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: ```bash sudo ufw allow OpenSSH sudo ufw allow 'Nginx Full' sudo ufw enable sudo ufw status ``` ```output 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: ```bash curl -sI http://localhost | head -n 1 ``` ```output HTTP/1.1 200 OK ``` ## Paso 2: Instalar MySQL Instala el servidor MySQL: ```bash sudo apt install -y mysql-server ``` Comprueba que está en marcha: ```bash sudo systemctl status mysql --no-pager mysql --version ``` ```output 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: ```bash 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. ```bash 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: ```bash php -v sudo systemctl status php8.3-fpm --no-pager ``` ```output 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: ```bash ls -l /run/php/php8.3-fpm.sock ``` ```output 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: ```bash sudo mkdir -p /var/www/your_domain sudo chown -R $USER:$USER /var/www/your_domain ``` Crea el archivo de configuración: ```bash sudo nano /etc/nginx/sites-available/your_domain ``` ```nginx 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: ```bash sudo ln -s /etc/nginx/sites-available/your_domain /etc/nginx/sites-enabled/ sudo unlink /etc/nginx/sites-enabled/default sudo nginx -t ``` ```output nginx: the configuration file /etc/nginx/nginx.conf syntax is ok nginx: configuration file /etc/nginx/nginx.conf test is successful ``` Recarga Nginx: ```bash sudo systemctl reload nginx ``` ## Paso 5: Crear una base de datos y un usuario Entra en MySQL: ```bash 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: ```sql 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: ```bash mysql -u app_user -p -e "SELECT * FROM app_db.tareas;" ``` ```output +----+------------------+ | 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: ```bash nano /var/www/your_domain/prueba.php ``` ```php PDO::ERRMODE_EXCEPTION] ); echo '
PHP ' . PHP_VERSION . ' (' . PHP_SAPI . ')
'; ``` Pídela desde la terminal o abre `http://your_domain/prueba.php` en el navegador: ```bash curl -s -H "Host: your_domain" http://localhost/prueba.php ``` ```outputPHP 8.3.6 (fpm-fcgi)
``` `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: ```bash 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: ```bash sudo nano /etc/php/8.3/fpm/php.ini ``` Busca estas directivas (con `Ctrl+W` en nano) y ajústalas: ```ini 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: ```bash ps --no-headers -o rss -C php-fpm8.3 | awk '{ sum += $1; n++ } END { printf "%.0f MB por proceso\n", sum / n / 1024 }' ``` ```output 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: ```bash sudo nano /etc/php/8.3/fpm/pool.d/www.conf ``` ```ini 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: ```bash sudo php-fpm8.3 -t sudo systemctl restart php8.3-fpm ``` ```output 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`: ```bash sudo nano /etc/nginx/conf.d/tuning.conf ``` ```nginx 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: ```bash sudo nginx -t sudo systemctl reload nginx ``` > Nota: si antes descomentaste `gzip_types` o `gzip_vary` en `/etc/nginx/nginx.conf`, `nginx -t` avisará 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: ```bash sudo apt install -y certbot python3-certbot-nginx ``` Comprueba que el dominio resuelve a tu servidor y solicita el certificado: ```bash 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: ```output 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: ```bash systemctl list-timers certbot.timer sudo certbot renew --dry-run ``` Verifica el acceso por HTTPS y que las cabeceras ya no revelan versiones: ```bash curl -sI https://your_domain | grep -iE '^(HTTP|server)' ``` ```output 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: ```bash sudo mkdir -p /var/backups/mysql sudo nano /usr/local/bin/mysql-backup.sh ``` ```bash #!/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: ```bash 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.