Indicamos cómo desplegar una infraestructura web profesional y de alta disponibilidad utilizando máquinas virtuales en Proxmox. En esta guía práctica, configuraremos paso a paso un balanceador de carga Nginx que reparte el tráfico entre dos servidores backend con PHP 8.5, todo ello optimizado con OPcache y protegido con reglas de firewall. Ideal para APIs REST, aplicaciones PHP y servicios que requieran escalabilidad y tolerancia a fallos sin recurrir a la nube.
- Servidor web de alta disponibilidad con balanceo de carga.
- Balanceador con Linux Ubuntu Server 26.04 LTS y Nginx.
- Configuración de cada nodo servidor web con Nginx.
- Configurar OPcache en los nodos.
- NAT en cortafuegos/router para redirigir las peticiones a la IP pública a la IP local del balanceador.
- Revisión de logs en balanceador y nodos.
- Punto de fallo en el balanceador.
Servidor web de alta disponibilidad con balanceo de carga
Vamos a desplegar desde cero un clúster web de balanceo activo-activo sobre tres máquinas virtuales, alojadas en un hipervisor Proxmox. El objetivo es proporcionar un servicio web (sitio web PHP, API REST PHP, etc.) de forma rápida, segura y con capacidad de seguir funcionando incluso si uno de los servidores falla.
La infraestructura se compone de:
- 1 Balanceador de Carga (webproxy): Recibe todas las peticiones de los clientes y las distribuye de forma inteligente entre los nodos de aplicación.
- 2 Servidores de Aplicación (webnodo1 y webnodo2): servidores web para ejecución de sitio web/servicio web, procesan las peticiones de los usuarios.
- 1 Servidor de Base de Datos (opcional, fuera del alcance de este artículo): Almacena los datos de la aplicación.
Todos los servidores se configuran en máquinas virtuales con sistema operativo Linux Ubuntu Server 26.04 LTS, Nginx como servidor web, PHP 8.5-FPM como motor de ejecución y OPcache para acelerar el rendimiento. La seguridad se refuerza con la activación del cortafuegos local en cada nodo.
La alta disponibilidad la proporcionan los nodos que procesan las peticiones de los usuarios. Por ello, debe existir alta disponibilidad a nivel de nodo (que haya dos o más nodos) y, a nivel de virtualización y almacenamiento, también debe existir alta disponibilidad. Si se despliegan los nodos en un sistema de virtualización sin alta disponibilidad, aunque haya varios, si el sistema de virtualización o el almacenamiento caen, caerán los nodos.
En el siguiente tutorial indicamos cómo desplegar un clúster de Proxmox para dotar de alta disponibilidad al sistema de virtualiación:
Y en este otro tutorial explicamos cómo habilitar en HA (alta disponibilidad) en máquinas virtuales Proxmox:
Para un entorno de producción que requiera alto rendimiento y alta disponibilidad, tanto el almacenamiento, como el sistema de virtualización y las propias máquinas virtuales, deben estar en HA (alta disponibilidad), con sus clústeres correspondientes.
Explicación del flujo de peticiones web
Para este estudio de caso, dispondremos de un balanceador y dos nodos. La URL pública será http://proyecto.com:8080/apirestweb y en los nodos, los ficheros del servicio web PHP, estarán en la carpeta /var/www/apirestweb/. Usaremos el puerto 8080, a modo de ejemplo, dado que se trata de un servicio que no queremos que esté en los puertos «estándar» 80 ó 443.
- El Cliente (navegador, app móvil, etc.) hace una petición HTTP a la URL pública de nuestro servicio web http://proyecto.com:8080/apirestweb.
- El Router/Firewall redirige el tráfico del puerto 8080 a la IP privada del balanceador
webproxy(192.168.1.129). - El Balanceador (Nginx) recibe la petición. Gracias a la configuración de
upstream, evalúa cuál de los dos backends tiene menos conexiones activas en ese momento (least_conn) y reenvía la petición al nodo seleccionado. - El Backend (Nginx + PHP-FPM) recibe la petición, busca el archivo PHP en la carpeta
/var/www/apirestweb/, lo ejecuta con PHP 8.5 (acelerado por OPcache) y devuelve el resultado al balanceador. - El Balanceador reenvía la respuesta al cliente que la solicitó.

Balanceador con Linux Ubuntu Server 26.04 LTS y Nginx
En primer lugar, crearemos la máquina virtual para el balanceador (webproxy 192.168.1.129) e instalaremos Linux Ubuntu Server 26.04 LTS:

Una vez instalado el SO Linux, accederemos por SSH y ejecutaremos los siguientes comandos, para actualizar los paquetes y para instalar Nginx:
|
1 2 |
sudo apt update && sudo apt upgrade -y sudo apt install nginx -y |
Configuraremos el servidor web Nginx en modo proxy, para que balancee (reparta) las peticiones web al resto de nodos. Para ello, crearemos el fichero de configuración:
|
1 |
sudo nano /etc/nginx/sites-available/api_balanceador |
Con el siguiente contenido, donde ya estamos definiendo las reglas de balanceo para los dos futuros nodos (192.168.1.130 y 192.168.1.131, aunque aún no existan):
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
# ============================================ # Balanceador de carga para Servicios web y API REST # webproxy: 192.168.1.129 # Backends: 192.168.1.130 (webnodo1) # 192.168.1.131 (webnodo2) # ============================================ upstream backend_api { # Método de balanceo: menor número de conexiones activas least_conn; # Servidores backend (puerto 80 interno) server 192.168.1.130:80 max_fails=3 fail_timeout=30s; server 192.168.1.131:80 max_fails=3 fail_timeout=30s; # Mantener conexiones vivas con los backends keepalive 32; } server { listen 8080; server_name proyectoa.com; access_log /var/log/nginx/balanceador_access.log; error_log /var/log/nginx/balanceador_error.log; location /apirestweb/ { proxy_pass http://backend_api; proxy_http_version 1.1; proxy_set_header Connection ""; # Cabeceras para que el backend conozca al cliente original proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; # Timeouts ajustados para APIs proxy_connect_timeout 5s; proxy_read_timeout 60s; proxy_send_timeout 60s; # Reintentar en otro backend si este falla proxy_next_upstream error timeout invalid_header http_500 http_502 http_503; } # Bloquear acceso a archivos ocultos location ~ /\. { deny all; } } |
Es recomendable, por seguridad, eliminar el sitio web por defecto de Nginx, con el comando:
|
1 |
sudo rm /etc/nginx/sites-enabled/default |
Habilitamos el sitio web del balanceador y recargamos Nginx:
|
1 2 |
sudo ln -s /etc/nginx/sites-available/api_balanceador /etc/nginx/sites-enabled/ sudo systemctl reload nginx |
Se debe limitar el acceso al balanceador con el cortafuegos local de Linux, permitiendo únicamente a los puertos estrictamente necesarios. Por un lado, los que hayamos indicado para el servicio web, habitualmente el 80 y el 443 (para futuro paso a HTTPS), por otro, algún puerto de acceso administrativo, como el SSH. En este estudio de caso usamos el puerto 8080 para el servicio web, por lo que únicamente abriremos este puerto y el SSH. Para abrir estos puertos y habilitar el cortafuegos local de Linux, ejecutaremos los comandos:
|
1 2 3 4 |
sudo apt install ufw -y sudo ufw allow 22 sudo ufw allow 8080/tcp sudo ufw enable |
Una buena práctica es limitar aún más la apertura de los puertos anteriores, indicando el rango de IP o la IP que tendrán acceso, en lugar de todas las IP. Por ejemplo, para el acceso SSH, si solo vamos a acceder desde el equipo con IP 192.168.1.10, estableceríamos en el cortafuegos:
|
1 |
sudo ufw allow from 192.168.1.10 to any port 22 proto tcp |
De esta forma, reducimos la superfice de exposición, permitiendo el acceso SSH para gestión/administración únicamente desde un equipo de la red LAN.
Accediendo con un navegador a la IP del balanceador y al puerto que hayamos indicado (8080 en este caso):
http://192.168.1.129:8080/
Si todo es correcto, nos devolverá la página inicial de Nginx.

Configuración de cada nodo servidor web con Nginx
En todos los nodos que queramos agregar al balanceador, instalaremos Nginx y soporte para PHP. Para ello, en cada nodo, ejecutaremos los comandos:
|
1 2 3 4 |
sudo apt update && sudo apt upgrade -y sudo apt install nginx php8.5-fpm php8.5-cli php8.5-common \ php8.5-mysql php8.5-curl php8.5-xml php8.5-mbstring \ php8.5-zip -y |
En caso de necesitar otras extensiones, como GD (para redimensionar imágenes), las instalaremos en cada nodo:
|
1 |
sudo apt install php8.5-gd -y |
Revisaremos que el servidor web Nginx del nodo funciona correctamente, accediendo con un navegador a la IP del nodo, debería devolver la web por defecto de Nginx:

Crearemos la carpeta que contendrá los PHP y demás ficheros que compondrán nuestro sitio web o servicio API Rest. Asignaremos los permisos correspondientes. En el estudio de caso que nos ocupa, crearemos la carpeta «apirestweb»:
|
1 2 3 |
sudo mkdir -p /var/www/apirestweb sudo chown -R www-data:www-data /var/www sudo chmod -R 755 /var/www |
Si ya tenemos los archivos que componen el sitio web o el servicio API Rest, los copiaremos a esta carpeta. Si no los tenemos y queremos dejar un fichero PHP de pruebas, podemos ejecutar el siguiente comando para crearlo:
|
1 |
echo "<?php echo 'Prueba de nodo1 de servidor web con balancedor, por proyectoa.com'; ?>" | sudo tee /var/www/apirestweb/index.php |
Eliminaremos el sitio web por defecto de Nginx, con:
|
1 |
sudo rm /etc/nginx/sites-enabled/default |
Y creamos el archivo de carga del sitio web de nuestro servicio API Rest o servicio web:
|
1 |
sudo nano /etc/nginx/sites-available/api_backend |
Pegaremos el siguiente contenido, previo cambio de la IP del balanceador, la IP del nodo y la URL del sitio web por las que hayamos establecido anteriormente:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
server { listen 80; server_name 192.168.1.130; root /var/www; #index index.php index.html; access_log /var/log/nginx/nodo1_access.log; error_log /var/log/nginx/nodo1_error.log; location ~ /\. { deny all; } location ~ \.php$ { include snippets/fastcgi-php.conf; fastcgi_pass unix:/run/php/php8.5-fpm.sock; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; } location / { try_files $uri $uri/ /apirestweb/index.php?$query_string; } location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|woff|woff2)$ { expires 30d; add_header Cache-Control "public, immutable"; } } |
En el nodo2 y restantes, cambiaremos las líneas de la IP del nodo y el nombre del fichero de log del nodo:
|
1 2 3 |
server_name 192.168.1.131; access_log /var/log/nginx/nodo2_access.log; error_log /var/log/nginx/nodo2_error.log; |
Activamos la configuración con:
|
1 2 |
sudo ln -s /etc/nginx/sites-available/api_backend /etc/nginx/sites-enabled/ sudo systemctl reload nginx |
Si queremos probar el nodo con el fichero index.php de ejemplo (si no tenemos otros ficheros para probar), deberemos añadir el index.php al fichero de configuración, para ello, editaremos
|
1 |
sudo nano /etc/nginx/sites-available/api_backend |
Y añadiremos esta línea dentro de «server»:
|
1 |
index index.php; |

Recargaremos Nginx con:
|
1 |
sudo systemctl reload nginx |
Y ahora, si la configuración es correcta, al navegar hacia la IP del nodo, nos devolverá el contenido del fichero index.php creado anteriormente:

Con esta comprobación, tenemos la certeza de que el servicio web del nodo responde correctamente. Ahora es recomendable configurar la seguridad de acceso. Lo habitual es permitir el acceso al nodo únicamente desde el balanceador (192.168.1.129), por los puertos 80 ó 443 si hemos configurado HTTPS. Y, como es lógico, para administración del nodo, conviene abrir el SSH a algún equipo de la red desde el que necesitemos conectarnos. En este estudio de caso permitimos el acceso SSH desde el equipo 192.168.1.10:
|
1 2 3 |
sudo ufw allow from 192.168.1.10 to any port 22 proto tcp sudo ufw allow from 192.168.1.129 to any port 80 proto tcp sudo ufw allow from 192.168.1.129 to any port 443 proto tcp |
Activaremos el cortafuegos (si nos estamos conectando por SSH desde algún equipo que no está en las reglas definidas anteriormente, se cortará la conexión):
|
1 |
sudo ufw enable |
Cuando hayamos finalizado las pruebas del nodo, eliminaremos tanto el fichero /var/apirestweb/index.php que hemos creado, como la línea index index.php; en el fichero /etc/nginx/sites-available/api_backend.
Si queremos copiar los ficheros del nodo1 al nodo2, desde el nod1, para hacer sincronizaciones, necesitaremos abrir el puerto de ssh en el cortafuegos del nodo2 para permitir la conexión desde el nodo1, con:
|
1 |
sudo ufw allow from 192.168.1.130 to any port 22 proto tcp |
Y en el nodo1, para pasar todos los ficheros de nuestro sitio/servicio web al nodo2 (incluyendo permisos y dejando ambas carpetas iguales), ejecutaremos:
|
1 |
rsync -avz --delete /var/www/apirestweb/ usuario@192.168.1.131:/var/www/apirestweb/ |
(Cambiaremos «usuario» por el usuario del nodo2 con permisos de administrador)
Si tenemos configurada OPCache en los nodos (como explicamos en el siguiente punto), tras hacer la actualización de ficheros de nuestro sitio web, conviene reiniciar el servicio PHP en todos los nodos:
|
1 |
sudo systemctl restart php8.5-fpm |
Desde el nodo1, para realizar el reinicio del servicio PHP-FPM en el nodo2, ejecutaremos:
|
1 |
ssh usuario@192.168.1.131 "sudo systemctl restart php8.5-fpm" |
(Cambiaremos «usuario» por el usuario del nodo2 con permisos de administrador)
Configurar OPcache en los nodos
OPcache es un sistema de caché de bytecode para PHP que viene incluido en el núcleo del lenguaje desde la versión 5.5. Su función principal es eliminar la necesidad de que PHP lea, analice y compile los scripts en cada petición, almacenando el código ya compilado en la memoria RAM del servidor. El resultado: una reducción drástica del tiempo de respuesta y un aumento significativo en el número de peticiones que el servidor puede manejar.
En un entorno sin OPcache, cada vez que un usuario solicita una página PHP ocurre lo siguiente:
- El servidor lee el archivo
.phpdel disco. - El motor de PHP analiza (parsea) el código fuente.
- Lo compila a un lenguaje intermedio llamado bytecode (código de operaciones).
- Finalmente, ejecuta ese bytecode y devuelve el resultado.
Con OPcache activado, los pasos 1, 2 y 3 solo ocurren la primera vez que se ejecuta el script. En las siguientes peticiones, PHP recupera directamente el bytecode desde la memoria RAM, saltándose la lectura del disco y el procesamiento.
Es muy recomendable configurar y activar OPCache en los nodos. Para activarlo, en cada nodo, crearemos el siguiente fichero:
|
1 |
sudo nano /etc/php/8.5/fpm/conf.d/10-opcache.ini |
Con el contenido:
|
1 2 3 4 5 6 |
extension=opcache.so opcache.enable=1 opcache.memory_consumption=128 opcache.max_accelerated_files=10000 opcache.validate_timestamps=0 opcache.revalidate_freq=0 |
OPcache es una extensión de PHP, no tiene que ver con Nginx, por lo que para recargarlo, ejecutaremos:
|
1 |
systemctl restart php8.5-fpm |
Repetiremos el proceso para el resto de nodos.
NAT en cortafuegos/router para redirigir las peticiones a la IP pública a la IP local del balanceador
Una vez que tengamos configurados los servidores: balanceador y nodos, podremos pasar a «producción» las peticiones desde Internet, de forma que, cuando se acceda a la URL pública, se envíen las peticiones al balanceador. Para ello, accederemos al router o cortafuegos de nuestra organización y configuraremos el NAT correspondiente. Según el router o cortafuegos, nos encontraremos la opción de NAT en sitios diferentes, también podemos encontrarla como Port Mapping (mapeo de puertos o redirección de puertos). En esta opción, estableceremos el puerto origen (en este caso 8080) y la IP y puerto de destino, en este caso la IP del balanceador 192.168.1.129 y el puerto 8080:

De esta forma, todas las peticiones que lleguen al router/cortafuegos de nuestra organización, serán derivadas al balanceador.
Revisión de logs en balanceador y nodos
Para comprobar que las peticiones están llegando al balanceador y que éste las distribuye a los nodos, revisaremos los logs de acceso.
En el caso del servidor de balanceo, podremos consultar los logs de acceso visualizando el contenido del fichero:
|
1 |
cat /var/log/nginx/balanceador_access.log |
En el access.log del balanceador, las peticiones llegarán desde IP de la LAN o de Internet:

Para ver los posibles errores, visualizaremos el fichero:
|
1 |
cat /var/log/nginx/balanceador_error.log |
En el caso de los nodos, podremos consultar si están recibiendo peticiones, visualizando el fichero:
|
1 2 |
cat /var/log/nginx/nodo1_access.log cat /var/log/nginx/nodo2_access.log |
En el caso de los nodos, las peticiones siempre llagarán desde el balanceador (192.168.1.129).


Punto de fallo en el balanceador
En todo este entorno de alta disponibilidad, existe un punto de fallo que es el balanceador. Si el balanceador cae, el servicio web dejará de funcionar, aunque los nodos estén operativos.
Hay varias formas de solucionar este punto de fallo, una de ellas, la más rápida aunque manual, es elaborar un protocolo para que, en caso de fallo, en el NAT del router/cortafuegos se cambie la IP de destino a la de uno de los nodos. De esta forma, mientras se soluciona la caída del balanceador, el servicio seguirá funcionando. El servicio quedará algo degradado por pasar toda la carga en un nodo pero, al menos, seguirá funcionando.
Otra opción, más profesional, automática y efectiva, es desplegar un segundo balanceador y usar Keepalived para generar IP virtual y que, si uno de los balanceadores cae, el otro seguirá recibiendo las peticiones. Esto lo explicaremos en otro tutorial.