Linux29 de agosto de 2026 6 vistas

Alojar varios sitios en un VPS con Nginx (2026)

Alojar varios sitios en un VPS con Nginx (2026)

Lo que implica "varios sitios en un VPS"

Alojar varios sitios en un VPS no es solo una cuestión de configuración de Nginx. Tres puntos que hay que anticipar antes de empezar:

  • RAM: cada sitio activo consume memoria (workers de PHP-FPM, base de datos, caché). Un VPS con 4 GB de RAM puede alojar varios sitios con poco tráfico, pero no decenas de sitios WordPress con tráfico simultáneo. Hay que dimensionar en función del tráfico real, no del número de sitios.
  • Aislamiento: en una misma instancia de PHP-FPM sin configuración específica, los sitios no están realmente aislados. Un sitio comprometido puede leer los archivos de los demás. La solución es utilizar pools de PHP-FPM separados con usuarios de sistema distintos (ver la sección dedicada más abajo).
  • Copias de seguridad: con varios sitios, las copias manuales se vuelven rápidamente inmanejables. Conviene automatizarlas desde el principio con un bucle sobre los directorios y las bases de datos.

Estructura de directorios recomendada

Conviene usar un directorio por sitio bajo /var/www/, con el nombre de dominio como identificador. Esta convención hace que los scripts de copia de seguridad y las configuraciones de Nginx sean legibles y predecibles.

/var/www/
    site-a.fr/
    site-b.fr/
    site-c.fr/

Crear los directorios:

sudo mkdir -p /var/www/site-a.fr
sudo mkdir -p /var/www/site-b.fr
sudo mkdir -p /var/www/site-c.fr

Los permisos se ajustarán al crear los pools de PHP-FPM (sección siguiente).

Un vhost de Nginx por sitio

Crear un archivo de configuración por sitio

Cada sitio tiene su propio archivo en /etc/nginx/sites-available/. No hay que ponerlo todo en un solo archivo: un archivo por sitio facilita la desactivación, la depuración y la lectura.

Ejemplo para site-a.fr:

sudo nano /etc/nginx/sites-available/site-a.fr
server {
    listen 80;
    listen [::]:80;
    server_name site-a.fr www.site-a.fr;
    root /var/www/site-a.fr;
    index index.php index.html;

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

    location ~ \.php$ {
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:/run/php/site-a.sock;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include fastcgi_params;
    }

    location ~ /\.(ht|git|env) {
        deny all;
    }
}

Repetir para cada sitio sustituyendo site-a.fr y el socket de PHP-FPM (site-a.sock) por los valores correspondientes.

Nota: el socket /run/php/site-a.sock todavía no existe en esta fase, se crea en el paso de los pools de PHP-FPM más abajo. nginx -t no comprueba la existencia del socket, así que la configuración se validará, pero el sitio devolverá un error 502 mientras el pool correspondiente no esté en su sitio.

Activar los sitios

sudo ln -s /etc/nginx/sites-available/site-a.fr /etc/nginx/sites-enabled/
sudo ln -s /etc/nginx/sites-available/site-b.fr /etc/nginx/sites-enabled/
sudo ln -s /etc/nginx/sites-available/site-c.fr /etc/nginx/sites-enabled/

Hay que probar siempre la configuración antes de recargar:

sudo nginx -t
sudo systemctl reload nginx

Fuente: nginx.org - directiva server_name

La trampa del primer vhost servido y el vhost catch-all

Lo que ocurre sin un vhost por defecto explícito

Cuando Nginx recibe una petición para un dominio o una IP que no está declarada en ningún server_name, sirve el primer vhost cargado por orden alfabético. Este comportamiento está documentado y es predecible, pero puede exponer un sitio involuntariamente: una petición a la IP en bruto del VPS, o a un dominio que apunta al VPS por error, acaba en el primer sitio configurado.

Fuente: nginx.org - How nginx processes a request

El vhost catch-all con default_server

Crear un vhost por defecto que intercepte todas las peticiones no reconocidas y devuelva una respuesta vacía (código 444, que cierra la conexión sin respuesta HTTP):

sudo nano /etc/nginx/sites-available/default-catchall
server {
    listen 80 default_server;
    listen [::]:80 default_server;
    listen 443 ssl default_server;
    listen [::]:443 ssl default_server;
    server_name _;

    # Certificado autofirmado para absorber las peticiones HTTPS no reconocidas
    ssl_certificate /etc/nginx/ssl/self-signed.crt;
    ssl_certificate_key /etc/nginx/ssl/self-signed.key;

    return 444;
}

Generar un certificado autofirmado para el bloque SSL del catch-all (Certbot no puede emitir un certificado para _):

sudo mkdir -p /etc/nginx/ssl
sudo openssl req -x509 -nodes -days 3650 -newkey rsa:2048 \
  -keyout /etc/nginx/ssl/self-signed.key \
  -out /etc/nginx/ssl/self-signed.crt \
  -subj "/CN=localhost"

Activar el catch-all y recargar:

sudo ln -s /etc/nginx/sites-available/default-catchall /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx

El código 444 cierra la conexión TCP sin enviar respuesta. Los escáneres automáticos y las peticiones a la IP en bruto no obtienen ninguna información sobre los sitios alojados.

Pools de PHP-FPM separados por sitio

¿Por qué pools separados?

Sin pools separados, todos los sitios se ejecutan bajo el mismo usuario del sistema (a menudo www-data). Un sitio comprometido puede leer los archivos de configuración de los demás (archivos .env, wp-config.php, claves de API). Los pools separados aportan:

  • Aislamiento de los permisos: cada sitio se ejecuta bajo su propio usuario
  • Límites de procesos por sitio: un sitio que recibe un pico de tráfico no consume todos los workers PHP disponibles
  • Registros separados: identificar rápidamente qué sitio genera errores

Fuente: php.net - Configuración de PHP-FPM

Crear un usuario de sistema por sitio

sudo useradd -r -s /usr/sbin/nologin site-a
sudo useradd -r -s /usr/sbin/nologin site-b
sudo useradd -r -s /usr/sbin/nologin site-c

Ajustar los permisos de los directorios:

sudo chown -R site-a:site-a /var/www/site-a.fr
sudo chown -R site-b:site-b /var/www/site-b.fr
sudo chown -R site-c:site-c /var/www/site-c.fr

Nginx (que se ejecuta bajo www-data) debe poder leer los archivos. Hay que añadir www-data a los grupos de los usuarios de sitio, o ajustar los permisos de los directorios:

sudo chmod 750 /var/www/site-a.fr
sudo usermod -aG site-a www-data

Conviene saberlo: añadir www-data al grupo de cada sitio permite a Nginx servir los archivos estáticos, pero eso también significa que el proceso de Nginx puede leer los archivos de todos los sitios. El aislamiento que aportan los pools afecta a la ejecución de PHP, no a la lectura por parte del servidor web. Para un aislamiento más estricto, hay que recurrir a contenedores o a máquinas virtuales distintas.

Crear un pool de PHP-FPM por sitio

Copiar el pool por defecto como base:

sudo cp /etc/php/8.4/fpm/pool.d/www.conf /etc/php/8.4/fpm/pool.d/site-a.conf
sudo nano /etc/php/8.4/fpm/pool.d/site-a.conf

Modificar estas directivas en el archivo (sustituir [www] por [site-a]):

[site-a]
user = site-a
group = site-a
listen = /run/php/site-a.sock
listen.owner = www-data
listen.group = www-data
pm = dynamic
pm.max_children = 5
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 3

Repetir para cada sitio (site-b.conf, site-c.conf) adaptando el nombre del pool, el usuario y el socket.

Desactivar el pool por defecto si todos los sitios tienen su propio pool:

sudo mv /etc/php/8.4/fpm/pool.d/www.conf /etc/php/8.4/fpm/pool.d/www.conf.disabled

Antes hay que comprobar que ningún vhost siga haciendo referencia al socket por defecto /run/php/php8.4-fpm.sock: esos sitios devolverían un error 502 tras la desactivación del pool.

Reiniciar PHP-FPM:

sudo systemctl restart php8.4-fpm

El socket de cada sitio (/run/php/site-a.sock) se corresponde con la directiva fastcgi_pass del vhost de Nginx correspondiente.

Bases de datos separadas por sitio

Una base de datos y un usuario de MariaDB dedicados por sitio. Nunca hay que usar un usuario compartido entre varios sitios: si un sitio se ve comprometido, se da acceso a todas las bases.

sudo mariadb -u root
-- Sitio A
CREATE DATABASE site_a_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'site_a_user'@'localhost' IDENTIFIED BY 'contrasena_segura_a';
GRANT ALL PRIVILEGES ON site_a_db.* TO 'site_a_user'@'localhost';

-- Sitio B
CREATE DATABASE site_b_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'site_b_user'@'localhost' IDENTIFIED BY 'contrasena_segura_b';
GRANT ALL PRIVILEGES ON site_b_db.* TO 'site_b_user'@'localhost';

FLUSH PRIVILEGES;
EXIT;

Certificados SSL multidominio

Certbot gestiona los certificados de cada dominio de forma independiente. Hay que emitir un certificado por dominio (o por pareja dominio/www):

sudo certbot --nginx -d site-a.fr -d www.site-a.fr
sudo certbot --nginx -d site-b.fr -d www.site-b.fr
sudo certbot --nginx -d site-c.fr -d www.site-c.fr

Certbot modifica automáticamente los vhosts de Nginx para añadir las directivas SSL y la redirección de HTTP a HTTPS.

La renovación automática la gestiona un temporizador systemd instalado por Certbot. Hay que comprobar que está activo:

sudo systemctl status certbot.timer

Guía completa (renovación, certificados wildcard, resolución de errores): Guía Certbot de OuiHeberg.

Dimensionamiento: ¿cuántos sitios con cuánta RAM?

No existe una respuesta universal: el número de sitios que se pueden alojar depende del tráfico de cada sitio, del CMS utilizado y de los plugins activos. Algunos órdenes de magnitud razonables:

  • Un sitio WordPress con OPcache activado y poco tráfico simultáneo consume entre 128 MB y 256 MB de RAM en carga normal.
  • Un sitio estático o una aplicación ligera consume claramente menos.
  • Un sitio WooCommerce con tráfico o con plugins pesados puede consumir 512 MB o más.

En un VPS con 4 GB de RAM, es razonable alojar varios sitios con poco tráfico. Conviene vigilar el consumo real con free -h y top tras la puesta en producción, y ajustar en consecuencia los valores de pm.max_children de los pools de PHP-FPM.

No hay que saturar la RAM disponible: hay que dejar un margen para el sistema operativo, MariaDB y los picos de carga imprevistos.

Copias de seguridad multisitio

Con varios sitios, conviene automatizar las copias de seguridad desde el principio. Un bucle sobre los directorios y las bases de datos lo cubre todo en una sola tarea cron.

Script de copia de seguridad

#!/bin/bash
BACKUP_DIR="/root/backups"
DATE=$(date +%F)
mkdir -p "$BACKUP_DIR"

# Copia de seguridad de los archivos de cada sitio
for SITE in /var/www/*/; do
    SITE_NAME=$(basename "$SITE")
    tar -czf "$BACKUP_DIR/${SITE_NAME}-files-${DATE}.tar.gz" "$SITE"
done

# Copia de seguridad de las bases de datos
for DB in site_a_db site_b_db site_c_db; do
    mysqldump -u root "$DB" > "$BACKUP_DIR/${DB}-${DATE}.sql"
done

# Eliminar las copias de más de 7 días
find "$BACKUP_DIR" -type f -mtime +7 -delete

Hay que guardar este script en /usr/local/bin/backup-sites.sh, hacerlo ejecutable y añadirlo al crontab:

sudo chmod +x /usr/local/bin/backup-sites.sh
sudo crontab -e
# Copia de seguridad diaria a las 3 de la madrugada
0 3 * * * /usr/local/bin/backup-sites.sh

Externalizar las copias de seguridad

Las copias almacenadas en el mismo VPS no protegen frente a un fallo de hardware o a un compromiso del servidor. Hay que transferir con regularidad los archivos comprimidos a una ubicación remota (almacenamiento de objetos, servidor de terceros, máquina local) mediante rsync o scp.

Preguntas frecuentes

¿Se pueden alojar sitios con versiones de PHP diferentes en el mismo VPS?

Sí. Hay que instalar varias versiones de PHP-FPM en paralelo (por ejemplo php8.1-fpm y php8.4-fpm) y después asignar a cada pool la versión correspondiente. El socket de cada pool apunta a la versión de PHP deseada, y el vhost de Nginx de cada sitio hace referencia al socket correcto.

sudo apt install php8.1-fpm php8.4-fpm

Cada versión tiene su propio directorio de pools: /etc/php/8.1/fpm/pool.d/ y /etc/php/8.4/fpm/pool.d/.

¿Un sitio que falla puede afectar a los demás?

Con pools de PHP-FPM separados y límites de pm.max_children por pool, un sitio que recibe un pico de tráfico o que genera errores PHP no consume los workers de los demás sitios. En cambio, si MariaDB se satura (demasiadas conexiones simultáneas), todos los sitios que dependen de ella pueden verse afectados. Hay que vigilar las conexiones de MariaDB con SHOW PROCESSLIST;.

¿Cómo desactivar temporalmente un sitio sin eliminarlo?

Hay que eliminar el enlace simbólico en sites-enabled y recargar Nginx:

sudo rm /etc/nginx/sites-enabled/site-a.fr
sudo nginx -t && sudo systemctl reload nginx

El archivo de configuración de sites-available se conserva. Para reactivarlo, basta con volver a crear el enlace simbólico.

¿Cómo añadir un nuevo sitio después de la configuración inicial?

Hay que crear el directorio, el usuario del sistema, el pool de PHP-FPM, la base de datos, el vhost de Nginx y el certificado SSL siguiendo las secciones de esta guía en orden. Cada incorporación es independiente y no afecta a los sitios existentes.

¿Son obligatorios los pools de PHP-FPM separados?

No, pero sí muy recomendables en cuanto varios sitios conviven en el mismo VPS. Sin pools separados, todos los sitios se ejecutan bajo www-data y pueden acceder a los archivos de los demás. Para un VPS personal con proyectos de confianza, un pool único puede bastar. Para sitios de clientes o proyectos de terceros, los pools separados son imprescindibles.