Saltar a contenido

Guía: despliegue multitenant con Nginx

En esta práctica vamos a montar el escenario: una sola instancia EC2 con Nginx sirviendo dos aplicaciones web distintas, cada una con su propio nombre de dominio:

  • planes.daw → RunDash (dashboard de planes de entrenamiento).
  • puertos.daw → Puertos Míticos (los grandes puertos del Tour de Francia).

A esto se le llama un despliegue multitenant (multi-inquilino): varios sitios comparten el mismo servidor. Es exactamente lo que hace un proveedor de hosting cuando aloja cientos de webs en una misma máquina.

Cómo leer esta guía: después de cada comando o bloque de configuración hay un apartado «¿Qué significa?» que explica línea a línea qué hace y por qué está ahí. No copies nada sin haberlo leído antes: en el examen y en la vida real nadie configura nada que no entiende.

1. El escenario que vamos a montar

flowchart TB
    Internet((Internet))
    PlanesReq["Host: planes.daw"]
    PuertosReq["Host: puertos.daw"]

    Internet -->|HTTP puerto 80| PlanesReq
    Internet -->|HTTP puerto 80| PuertosReq

    PlanesReq --> EC2
    PuertosReq --> EC2

    subgraph EC2["Instancia EC2 — Ubuntu Server 24.04, t2.small — «daw»"]
        direction TB
        Nginx["Nginx"]
        PlanesVH["server_name planes.daw"]
        PuertosVH["server_name puertos.daw"]
        PlanesApp["/var/www/planes (RunDash)"]
        PuertosApp["/var/www/puertos (Puertos)"]

        Nginx --> PlanesVH
        Nginx --> PuertosVH
        PlanesVH --> PlanesApp
        PuertosVH --> PuertosApp
    end

    SSH["Acceso SSH (puerto 22) con clave .pem"] -.-> EC2

El resultado final será:

  • Una instancia EC2 con Ubuntu Server 24.04 LTS.
  • Un security group que permite SSH (22) y HTTP (80) desde cualquier origen.
  • Un par de claves (.pem) para entrar por SSH.
  • Dos aplicaciones clonadas en directorios separados:
  • RunDash en /var/www/planes
  • Puertos Míticos en /var/www/puertos
  • Nginx con dos virtual hosts: uno responde a planes.daw y otro a puertos.daw.
  • Tu máquina local resolviendo ambos nombres hacia la IP de la instancia mediante el fichero /etc/hosts.

1.1. ¿Qué es el multitenancy y cómo lo consigue Nginx?

Multitenancy significa que un mismo servidor aloja varios sitios independientes (cada sitio es un tenant o inquilino). Cada inquilino tiene su propio contenido y su propio nombre, y ninguno sabe que convive con los demás.

La pieza clave es cómo Nginx decide qué sitio servir cuando llega una petición. Fíjate: ambas webs viven en la misma máquina, en el mismo puerto 80. ¿Cómo distingue Nginx a cuál de las dos va dirigida cada petición?

La respuesta está en la cabecera Host del HTTP. Cuando tu navegador pide una página, no solo dice «dame /»: también envía el nombre del sitio que quiere:

GET / HTTP/1.1
Host: planes.daw

Nginx lee esa cabecera y la compara con el server_name de cada uno de sus bloques server. El bloque cuyo server_name coincida es el que sirve la petición. A cada uno de esos bloques se le llama virtual host (host virtual): un sitio «virtual» dentro de un servidor físico.

Los virtual hosts basados en el nombre (server_name) se llaman name-based virtual hosts. Son los que usa todo el mundo: con una sola IP y un solo puerto puedes servir miles de sitios distintos.

1.2. El problema del DNS (y su solución)

Para que el navegador pueda enviar Host: planes.daw, primero necesita saber la IP de planes.daw. Normalmente eso lo hace el DNS (el «servicio de teléfonos» de Internet: traduce nombres a IPs).

Pero planes.daw y puertos.daw no son dominios reales: no están registrados ni hay ningún servidor DNS en el mundo que los conozca (además, .daw no es un TLD público). Si el navegador pregunta al DNS, obtendrá «no existe» y no podrá conectar.

La solución para la práctica: no usar el DNS público, sino el fichero local /etc/hosts. Ese fichero es una tabla nombre→IP que el sistema consulta antes que el DNS. Si ahí ponemos:

<IP>  planes.daw puertos.daw

nuestro ordenador resolverá ambos nombres hacia la IP de la instancia, sin necesidad de DNS real. Es el truco estándar para probar dominios que aún no existen.

1.3. Conceptos que vamos a usar continuamente

Concepto Qué es
Multitenancy Un servidor aloja varios sitios independientes (inquilinos).
Virtual host Cada sitio que Nginx sirve; se define con un bloque server.
server_name Directiva que dice a qué nombre de dominio responde un server.
Cabecera Host Cabecera HTTP con la que el cliente indica qué sitio pide. Nginx la usa para elegir el virtual host.
DNS Servicio que traduce nombres de dominio en direcciones IP.
/etc/hosts Fichero local con traducciones nombre→IP que se consultan antes que el DNS.
Document root (root) Directorio del disco de donde un sitio sirve sus ficheros. Cada tenant tiene el suyo.

Región de trabajo: us-east-1 (N. Virginia). AWS está dividida en regiones; los recursos que crees solo existen en la región seleccionada. Asegúrate de tenerla elegida en la esquina superior derecha de la consola antes de empezar.

2. Requisitos previos

  • Cuenta de AWS con permisos para crear instancias EC2, security groups y key pairs. Vale la capa gratuita (free tier).
  • Un cliente SSH instalado (Linux/macOS lo trae de serie; en Windows usa PowerShell o WSL, no PuTTY, para poder seguir los comandos tal cual).
  • Navegador web y curl para comprobar el resultado.
  • Poder editar el fichero /etc/hosts de tu máquina (necesita permisos de administrador).

3. Lanzar la instancia EC2

3.1. Abrir el asistente

  1. Entra en la consola de AWS → servicio EC2 → botón Launch instance.

3.2. Nombre y AMI

  1. Name: daw. Es solo una etiqueta para identificar la instancia en la consola. Usar nombres consistentes es buena práctica: ayuda a localizarla fácilmente en la consola y en los logs.

  2. En Amazon Machine Image (AMI) busca y selecciona Ubuntu Server 24.04 LTS (x86_64).

  3. Una AMI es la imagen base del disco. Elegimos Ubuntu Server porque todo el software que necesitamos (Nginx, git, python3) está en sus repositorios.
  4. LTS (Long Term Support): Canonical la mantiene 5 años con actualizaciones de seguridad.
  5. x86_64/amd64: las instancias t2 son x86_64, así que la imagen debe ser de esa arquitectura (no ARM).
  6. El propietario de las imágenes oficiales de Canonical es 099720109477; usar imágenes oficiales evita instalar software de origen dudoso.

3.3. Tipo de instancia

  1. Instance type: t2.small (1 vCPU, 2 GiB de RAM). Para servir dos sitios estáticos con Nginx sobra máquina; una más grande solo haría la práctica más cara sin aportar nada.

3.4. Key pair (par de claves)

  1. En Key pairs pulsa Create new key pair:
  2. Key pair name: daw-key
  3. Key pair type: RSA
  4. Private key file format: .pem
  5. Pulsa Create key pair. Se descargará daw-key.pem. Guárdalo bien: es la única vez que se puede descargar.

¿Por qué solo una vez? Porque AWS no guarda la clave privada: solo almacena la pública. Al arrancar la instancia, AWS inyecta la pública en /home/ubuntu/.ssh/authorized_keys. Al conectarte, tu cliente demuestra que posee la privada correspondiente sin que la clave viaje por la red. Si pierdes el .pem, nadie puede recuperarlo.

3.5. Security group (firewall)

  1. En Network settings → Edit marca Create security group y añade estas reglas de entrada (inbound):
Tipo Puerto Origen Descripción
SSH 22 0.0.0.0/0 SSH
HTTP 80 0.0.0.0/0 HTTP
  • Un security group es un firewall con estado: todo lo no permitido explícitamente queda bloqueado. Sin el 22 no entras por SSH; sin el 80 nadie ve las webs.
  • El origen va en notación CIDR. 0.0.0.0/0 = cualquier dirección IPv4. Para SSH sería más seguro tu IP (x.y.z.w/32), pero usamos 0.0.0.0/0 para que funcione desde cualquier red.
  • La regla de salida por defecto permite todo el tráfico saliente (necesario para apt y git).

3.6. Disco

  1. En Configure storage deja el disco raíz en 8 GiB (gp3). Para un Ubuntu con Nginx y dos sitios estáticos de unos pocos MB sobra espacio.

3.7. User data (cloud-init) — opcional pero recomendado

  1. Despliega Advanced details y en User data pega exactamente esto:
#cloud-config
hostname: daw
package_update: true
packages:
  - python3
  - python3-apt

El user data es un texto que AWS entrega a la instancia en su primer arranque. Si empieza por #cloud-config, cloud-init lo interpreta como instrucciones YAML y las ejecuta una sola vez:

Línea Significado
#cloud-config Cabecera mágica: le dice a cloud-init que el fichero es configuración suya. Sin ella, lo ignora.
hostname: daw Cambia el nombre de la máquina a daw. Sirve para identificarla en el prompt y en logs.
package_update: true Equivale a apt update: refresca el índice de paquetes.
packages: [python3, python3-apt] Instala el intérprete de Python 3 y el módulo que permite a Python interactuar con apt.

Si prefieres, sáltalo y haz el paso 5.1 completo en la máquina.

3.8. Lanzar

  1. Pulsa Launch instance y espera los dos estados:

    • Instance state: de pending a running.
    • Status checks: las dos comprobaciones en verde (2/2).
  2. Copia la Public IPv4 address. La llamaremos <IP> en el resto de la guía. Es efímera: si detienes y reanudas la instancia, cambia.

4. Conectar por SSH

Desde tu máquina local, ve al directorio donde descargaste la clave y ejecuta:

chmod 600 daw-key.pem
ssh -i daw-key.pem ubuntu@<IP>
  • chmod 600 deja la clave legible solo por tu usuario. SSH rechaza claves con permisos más abiertos: si cualquiera pudiera leer tu clave privada, podría hacerse pasar por ti.
  • -i daw-key.pem indica la clave privada con la que autenticarte.
  • ubuntu@<IP>: las imágenes de Ubuntu en AWS crean el usuario ubuntu (no se entra directo como root); luego usaremos sudo.

La primera vez preguntará Are you sure you want to continue connecting (yes/no)?. Escribe yes: SSH guarda la huella del servidor en ~/.ssh/known_hosts para detectar suplantaciones en el futuro.

Si falla, mira la sección 9. Solución de problemas.

5. Configurar la máquina

Todos los comandos siguientes se ejecutan dentro de la sesión SSH, casi todos con sudo (instalar paquetes y escribir en /etc, /var/www requiere ser root).

5.1. Nombre de host y paquetes base

Si no pusiste el user data del paso 3.7, hazlo a mano:

sudo hostnamectl set-hostname daw

hostnamectl set-hostname daw fija el nombre de la máquina a daw.

sudo apt update

apt update no instala nada: descarga la lista actualizada de paquetes de los repositorios. Hay que ejecutarlo en una máquina recién creada porque el índice de fábrica está desactualizado.

sudo apt install -y python3 python3-apt

Instala el intérprete Python y las librerías que permiten a Python usar apt. -y responde «sí» a la confirmación.

Instala ahora Nginx y git:

sudo apt install -y Nginx git
  • Nginx: el servidor web. Escuchará en el puerto 80 y servirá los dos sitios. En Ubuntu, al instalarlo, arranca solo y se habilita para el arranque.
  • git: para clonar las dos aplicaciones desde GitHub.

Si apt se bloquea esperando un lock (Could not get lock /var/lib/dpkg/lock-frontend), cloud-init sigue trabajando en segundo plano. Espera uno o dos minutos y reintenta; vigílalo con cloud-init status (termina en done).

5.2. Descargar las dos aplicaciones

En un escenario multitenant cada inquilino tiene su propio directorio. Vamos a clonar cada aplicación en el suyo, dentro de /var/www (el lugar convencional para contenido web en Linux):

sudo git clone https://github.com/raul-profesor/rundash.git /var/www/planes

Clona RunDash (el dashboard de planes de entrenamiento, un sitio estático generado con Astro) en /var/www/planes. Lo llamaremos planes porque es el sitio que responderá a planes.daw.

sudo git clone https://github.com/raul-profesor/puertos-tour.git /var/www/puertos

Clona Puertos Míticos (una SPA estática sobre los puertos del Tour) en /var/www/puertos. Responderá a puertos.daw.

¿Qué significa git clone <url> <directorio>? Descarga un repositorio completo (código e historial) desde GitHub al directorio indicado. Usamos sudo porque /var/www pertenece a root. El directorio de destino es el punto donde «se encuentran» el código y el servidor web: debe coincidir con el root de la configuración Nginx.

Comprueba que ambos tienen su index.html:

ls /var/www/planes/index.html /var/www/puertos/index.html

5.3. La anatomía de Nginx en Ubuntu: qué hay y para qué

Antes de escribir un solo fichero, conviene saber dónde vive todo dentro de Nginx. En Ubuntu, al instalar Nginx con apt, se crea esta estructura de directorios:

/etc/Nginx/                 ← raíz de la configuración
├── Nginx.conf               ← fichero principal (motor + includes)
├── sites-available/         ← configs de TODOS los sitios (se escriben aquí)
├── sites-enabled/           ← enlaces a los sitios ACTIVOS (lo que Nginx lee)
├── snippets/                ← fragmentos reutilizables
├── conf.d/                  ← configs extra (no son virtual hosts)
└── mime.types               ← tabla extensión → tipo MIME

/var/www/                    ← contenido web (document roots)

/var/log/Nginx/              ← logs
├── access.log               ← registro de cada petición HTTP
└── error.log                ← errores y problemas

/run/Nginx.pid               ← PID del proceso maestro (lo crea Nginx)

5.3.1. ¿Qué hace cada cosa y por qué está ahí?

Ruta Qué contiene Para qué
/etc/Nginx/Nginx.conf El fichero principal de configuración Define el motor (procesos, eventos, conexiones, compresión, formato de logs por defecto…) y al final contiene una línea include /etc/Nginx/sites-enabled/*; que «engancha» nuestros virtual hosts. No lo vamos a tocar: para eso están los ficheros por sitio.
/etc/Nginx/sites-available/ Ficheros de configuración de todos los sitios que podrían existir Aquí «se escriben» las configs. Tenerlas en este directorio no significa que se sirvan: es como tener una receta guardada en una carpeta de pendientes.
/etc/Nginx/sites-enabled/ Enlaces simbólicos a sites-available/ Lo que Nginx realmente lee al arrancar. Si no hay enlace aquí, el sitio no se sirve aunque su fichero exista. Activar = crear el enlace; apagar = borrar el enlace (la receta original se queda en sites-available).
/etc/Nginx/snippets/ Trozos de configuración reutilizables Pequeños fragmentos que se incluyen desde varios sitios (por ejemplo, una cabecera de seguridad común). No lo usamos aquí, pero conviene saber que existe si más adelante ves directivas tipo include snippets/security-headers.conf;.
/etc/Nginx/conf.d/ Configuraciones adicionales Pensado para configs que no son virtual hosts (parámetros de proxy, mapas, upstreams…). En Ubuntu la convención es sites-available para hosts y conf.d para el resto.
/etc/Nginx/mime.types Tabla extensión ↔ Content-Type Para que Nginx responda con la cabecera Content-Type correcta: .html → text/html, .css → text/css, .js → application/javascript, .png → image/png… Si un fichero se descarga en vez de abrirse en el navegador, casi siempre es porque falta su tipo MIME.
/var/www/ Contenido web Por convención en Linux, los document roots viven aquí. Otros directorios (/srv, /home) son posibles, pero /var/www es lo estándar en Debian/Ubuntu y lo que Nginx asume por defecto.
/var/log/Nginx/access.log Una línea por cada petición HTTP recibida IP del cliente, fecha, método, ruta, código de respuesta, bytes enviados, referer y user-agent. Es lo primero que miras cuando algo «no carga».
/var/log/Nginx/error.log Errores y avisos Fallos de sintaxis, 404s, problemas al abrir ficheros, timeouts, permisos… Cualquier cosa que Nginx no haya podido hacer bien acaba aquí.
/run/Nginx.pid Un número: el PID del proceso maestro Nginx lo escribe al arrancar. systemctl lo lee para enviarle señales (reload, stop). No lo editas a mano.

5.3.2. El patrón sites-available / sites-enabled con más detalle

¿Por qué Nginx en Ubuntu se instala con dos directorios para los sitios y no con uno? Porque te interesa poder tener configs escritas pero desactivadas:

  • Escribes la config en sites-available/ → el sitio existe en disco pero no se sirve.
  • Creas el enlace en sites-enabled/ → Nginx lo lee en el siguiente reload y empieza a servirlo.
  • Borras el enlace de sites-enabled/ → Nginx deja de servirlo en el siguiente reload, pero la config sigue en sites-available lista para reactivarla con un solo ln -s.

Este mecanismo se conecta con Nginx.conf mediante una única línea al final del bloque http { ... }:

include /etc/Nginx/sites-enabled/*;

Es un enganche: Nginx lee su propio motor y, al final, «barra» todo lo que haya en sites-enabled. Cada fichero que encuentre se convierte en un bloque server { ... } más.

Si alguna vez sigues un tutorial que use solo un directorio (típico en CentOS/RHEL o Alpine) no es un error: Nginx solo entiende la directiva include, no los nombres. La separación available / enabled es una convención de Debian/Ubuntu, no una exigencia del software.

5.3.3. ¿Y qué hay dentro de Nginx.conf?

Aunque no lo vamos a editar, ayuda saber qué piezas tiene cuando lo abras. Su estructura básica es:

user www-data;                  # usuario con el que corren los workers
worker_processes auto;          # nº de procesos worker (auto = nº de CPUs)
pid /run/Nginx.pid;             # ruta del fichero PID
events {
    worker_connections 768;     # conexiones simultáneas por worker
}
http {
    # ... configuración global: tipos MIME, formato de logs por defecto,
    # compresión, timeouts, cabeceras por defecto...
    include /etc/Nginx/mime.types;
    include /etc/Nginx/conf.d/*.conf;
    include /etc/Nginx/sites-enabled/*;   # ← aquí se enganchan nuestros sitios
}

La directiva clave es la última: el include que une el motor con los virtual hosts que vamos a crear en los siguientes apartados. El bloque http { ... } se cierra justo después, así que todo lo que metamos en sites-enabled queda automáticamente dentro de ese contexto HTTP.

5.4. Configurar los virtual hosts

Antes de escribir la configuración, recuerda cómo se organiza Nginx en Ubuntu:

  • /etc/nginx/nginx.conf es el fichero principal; al final incluye todo lo que haya en /etc/nginx/sites-enabled/.
  • /etc/nginx/sites-available/ guarda las configuraciones de todos los sitios.
  • /etc/nginx/sites-enabled/ contiene enlaces simbólicos a los sitios activos.
  • Esta separación permite tener sitios definidos pero apagados: se activan/desactivan creando/borrando el enlace.

Vamos a crear dos ficheros, uno por tenant.

5.4.1. Sitio planes.daw

sudo nano /etc/Nginx/sites-available/planes.daw

Pega este contenido:

server {
    listen 80;
    listen [::]:80;
    server_name planes.daw;

    root /var/www/planes;
    index index.html;

    location / {
        try_files $uri $uri/index.html $uri/ =404;
    }
}

¿Qué significa cada línea?

Línea Significado
server { ... } Define un virtual host: un sitio completo. Un mismo Nginx puede servir muchos server a la vez.
listen 80; Escucha en el puerto 80 (HTTP) en todas las IPv4.
listen [::]:80; Lo mismo para IPv6.
server_name planes.daw; La clave del multitenancy. Este bloque solo atiende peticiones cuya cabecera Host sea planes.daw. Así Nginx sabe que esta petición es para RunDash y no para la otra web.
root /var/www/planes; El document root de este tenant: dónde están sus ficheros. Debe coincidir con el directorio donde clonaste RunDash.
index index.html; El fichero por defecto al pedir un directorio.
location / { ... } Regla para todas las rutas.
try_files $uri $uri/index.html $uri/ =404; Prueba en orden y sirve lo primero que exista: el fichero exacto, luego <ruta>/index.html, luego el directorio; si nada existe, 404.

¿Por qué try_files con $uri/index.html? Porque Astro (RunDash) genera las rutas como directorios con un index.html dentro: /planes/5k/ es en realidad el fichero /var/www/planes/planes/5k/index.html. Sin esta regla, esas URLs darían 404.

Guarda (Ctrl+O, Enter, Ctrl+X).

5.4.2. Sitio puertos.daw

sudo nano /etc/Nginx/sites-available/puertos.daw

Pega este contenido:

server {
    listen 80;
    listen [::]:80;
    server_name puertos.daw;

    root /var/www/puertos;
    index index.html;

    location / {
        try_files $uri $uri/index.html $uri/ =404;
    }
}

Es idéntico al anterior salvo server_name y root: ahora el bloque responde a puertos.daw y sirve /var/www/puertos.

Puertos Míticos es una SPA con rutas «con almohadilla» (#/puertos): esas rutas las gestiona el navegador dentro de la misma página, así que al servidor siempre le llegan peticiones a ficheros reales (/, css/…, js/…, images/…). El mismo try_files sirve perfectamente.

Guarda y sal.

5.5. Activar los sitios y recargar Nginx

Activa ambos sitios creando los enlaces simbólicos y desactiva el sitio de ejemplo:

sudo ln -s /etc/Nginx/sites-available/planes.daw /etc/Nginx/sites-enabled/planes.daw
sudo ln -s /etc/Nginx/sites-available/puertos.daw /etc/Nginx/sites-enabled/puertos.daw
sudo rm /etc/Nginx/sites-enabled/default
  • ln -s origen destino crea un enlace simbólico (un acceso directo): el fichero real vive en sites-available y Nginx lo lee a través del enlace en sites-enabled.
  • Borramos default porque está marcado como default_server en el puerto 80 y sirve /var/www/html: si lo dejamos, respondería él a las peticiones que no coincidan con ningún server_name.

Valida la sintaxis de toda la configuración antes de aplicarla:

sudo nginx -t

Debe terminar con syntax is ok y test is successful. Se hace siempre antes de recargar: si recargáramos con un error, podríamos dejar el servidor caído. Si da error, te dirá el fichero y la línea.

Recarga y habilita Nginx:

sudo systemctl reload Nginx
sudo systemctl enable Nginx
  • reload le dice a Nginx que relea su configuración sin detenerse: los procesos antiguos terminan lo que tenían entre manos y los nuevos usan la config nueva. No corta las conexiones activas (a diferencia de restart).
  • enable hace que Nginx arranque solo al encender la máquina.

Comprueba que Nginx escucha en el 80:

ss -tlnp | grep :80

ss muestra los sockets de red (-t TCP, -l escuchando, -n puertos numéricos, -p proceso). Debes ver Nginx escuchando en 0.0.0.0:80 y/o [::]:80.

5.5.1. Dónde mirar cuando algo falla: los logs de Nginx

Nginx no es muy hablador en pantalla, pero lo registra todo en disco. Estos dos ficheros son tu primera parada cuando algo no va. Aprende a mirarlos antes de necesitarlos de verdad: cuando llegue el momento del fallo, ya sabrás dónde buscar.

Ver los errores en tiempo real (mientras reproduces el fallo):

sudo tail -f /var/log/Nginx/error.log

tail -f muestra las últimas líneas del fichero y se queda «siguiendo» los cambios: cada nuevo error aparece en pantalla al instante. Es ideal para reproducir un fallo mientras lo vigilas (abres el navegador, intentas cargar la página, y ves aparecer el error según ocurre).

Ver las últimas peticiones atendidas:

sudo tail -n 50 /var/log/Nginx/access.log

Las últimas 50 peticiones que ha atendido Nginx. Sirve para responder preguntas como «¿ha llegado siquiera mi petición?», «¿con qué código ha respondido?», «¿desde qué IP?».

Cómo leer una línea de access.log

Por defecto, Ubuntu configura Nginx con el formato combined, y cada línea tiene este aspecto:

203.0.113.42 - - [18/Sep/2026:12:34:56 +0000] "GET / HTTP/1.1" 200 1234 "-" "Mozilla/5.0 ..."
Campo Significado
203.0.113.42 IP del cliente que hizo la petición.
- - Identidad y usuario HTTP autenticado (normalmente vacíos, -).
[18/Sep/2026:12:34:56 +0000] Fecha y hora del servidor con zona horaria.
"GET / HTTP/1.1" Método, ruta y versión del protocolo.
200 Código de estado de la respuesta (200 OK, 404 Not Found, 500 Server Error…).
1234 Bytes enviados al cliente (cuerpo de la respuesta, no cabeceras).
"-" URL de la que venía el visitante (referer).
"Mozilla/5.0 ..." User-agent: navegador y sistema operativo del cliente.

Cómo leer una línea de error.log

2026/09/18 12:35:01 [error] 1234#0: *5 open() "/var/www/planes/missing.html" failed (2: No such file or directory), client: 203.0.113.42, server: planes.daw, request: "GET /missing.html HTTP/1.1", host: "planes.daw"
Campo Significado
2026/09/18 12:35:01 Fecha y hora del servidor.
[error] Nivel de severidad: emerg, alert, crit, error, warn, notice, info.
1234#0 PID del proceso worker y nº de thread que lo generó.
*5 Identificador interno de la conexión en Nginx (útil para seguir un mismo cliente en varias líneas).
open() "..." failed (2: No such file or directory) La operación que ha fallado y el motivo. El número entre paréntesis es el errno: 2 = ENOENT (no existe), 13 = EACCES (permiso denegado), 21 = EISDIR (es un directorio).
client: 203.0.113.42 IP del cliente que lo provocó.
server: planes.daw Qué virtual host estaba activo cuando ocurrió.
request: "GET /missing.html" Qué pidió exactamente.
host: "planes.daw" Cabecera Host que Nginx recibió.

Truco: si un sitio da 404, el error aparece en error.log, pero también puedes encontrarlo en access.log buscando el código 404:

sudo grep " 404 " /var/log/Nginx/access.log

Ahí verás qué ruta exacta pidió el cliente y desde qué IP, sin tener que rebuscar entre los demás niveles del log de errores.

6. Configurar la resolución de nombres en tu máquina

Hasta aquí el servidor está listo, pero tu ordenador aún no sabe qué IP tienen planes.daw y puertos.daw. Como no son dominios reales, lo resolvemos en el fichero /etc/hosts de tu máquina local (no en la instancia).

Sal de la sesión SSH (exit) y edita el fichero con permisos de administrador:

  • Linux / macOS:
sudo nano /etc/hosts
  • Windows: abre el Bloc de notas como administrador y abre el fichero C:\Windows\System32\drivers\etc\hosts.

Añade esta línea al final (sustituye <IP> por la IP pública de la instancia):

<IP>  planes.daw puertos.daw

Guarda y cierra.

¿Qué significa? /etc/hosts es una tabla local nombre→IP que el sistema operativo consulta antes que el DNS. Con esa línea, cada vez que una aplicación de tu máquina pregunte «¿qué IP tiene planes.daw?», el sistema responderá <IP> sin salir a Internet. Así el navegador podrá conectar y enviar la cabecera Host correcta.

Comprueba que tu máquina ya resuelve los nombres:

ping -c 1 planes.daw

Debe mostrar la IP de la instancia (pulsa Ctrl+C para parar si no se detiene solo).

Opción con Docker

docker run -d \
  --name firefox \
  --add-host planes.daw:[IP] ] \
  --add-host puertos.daw:[IP] \
  -p 5800:5800 \
  jlesage/firefox:latest

7. Verificación final

7.1. Con curl

Desde tu máquina local:

curl -I http://planes.daw

curl -I pide solo las cabeceras (método HEAD). Debes obtener HTTP/1.1 200 OK y Server: Nginx. Repite con el otro sitio:

curl -I http://puertos.daw

Ambos deben dar 200 OK. Aunque los dos viven en la misma máquina, cada uno responde por su nombre.

7.2. En el navegador

Abre http://planes.daw: verás el dashboard de RunDash, y cada plan debe abrir su página de detalle (eso demuestra que el try_files funciona). Abre http://puertos.daw: verás Puertos Míticos. Dos webs distintas, un solo servidor.

Si estás usando la opción de Docker, abre http://localhost:5800 y verás Firefox en tu navegador. Desde ahí abre http://planes.daw y http://puertos.daw para ver que funcionan.

7.3. Demostrar que Nginx elige por la cabecera Host

Puedes ver el mecanismo sin depender del navegador. Pide a la IP directa, pero forzando la cabecera Host:

curl -sI -H "Host: puertos.daw" http://<IP>/ | head -1
curl -sI -H "Host: planes.daw"  http://<IP>/ | head -1

-H "Host: …" añade esa cabecera a la petición. Aunque ambas van a la misma IP, Nginx devuelve el sitio cuyo server_name coincide. Es la prueba definitiva de que el enrutado lo hace la cabecera Host.

7.4. ¿Qué pasa si entras por la IP, sin nombre?

curl -sI http://<IP>/ | head -1

Ningún server_name coincide con una IP, así que Nginx usa el servidor por defecto del puerto 80: el bloque marcado default_server o, si no hay ninguno, el primer server que se carga. Como sites-enabled se lee por orden alfabético y planes.daw va antes que puertos.daw, el sitio por defecto será RunDash. Es un comportamiento normal; por eso conviene saber qué sitio queda como «cajón de sastre».

7.5. Verificar que Nginx registra las peticiones en access.log

Esta prueba cierra el círculo y te enseña a leer lo que Nginx ha visto. Hay que hacerla en una segunda terminal SSH contra la instancia (no en tu máquina local).

En la terminal SSH contra la instancia, deja el log a la vista mientras se actualiza:

sudo tail -f /var/log/nginx/access.log

Pulsa Enter un par de veces para dejar unas líneas en blanco y que se vea mejor dónde empieza cada petición nueva. Ahora, en tu máquina local (no en la sesión SSH), lanza peticiones:

curl -s -o /dev/null http://planes.daw/
curl -s -o /dev/null http://puertos.daw/
curl -s -o /dev/null http://planes.daw/planes/5k/

Vuelve a la terminal SSH: deberías ver cómo han ido apareciendo, en tiempo real, tres líneas nuevas con este aspecto (los números varían):

203.0.113.42 - - [18/Sep/2026:12:40:01 +0000] "GET / HTTP/1.1" 200 4521 "-" "curl/8.5.0"
203.0.113.42 - - [18/Sep/2026:12:40:03 +0000] "GET / HTTP/1.1" 200 7820 "-" "curl/8.5.0"
203.0.113.42 - - [18/Sep/2026:12:40:08 +0000] "GET /planes/5k/ HTTP/1.1" 200 1102 "-" "curl/8.5.0"

Lo importante:

  • La IP es la de tu máquina local (en AWS verás una IP pública; en una red de clase verás una IP privada 10.x o 192.168.x).
  • El código 200 en las tres líneas confirma que Nginx resolvió cada petición al virtual host correcto y encontró el fichero (incluida la ruta /planes/5k/, que gracias al try_files $uri/index.html se sirve desde /var/www/planes/planes/5k/index.html).
  • Si una ruta no existiera, verías 404 en lugar de 200 y, además, una línea correspondiente en error.log.

Para salir de tail -f: Ctrl+C.

Si quieres ver también el user-agent real del navegador (no el de curl), abre http://planes.daw en el navegador y observa cómo aparece una línea con Mozilla/5.0 ... como user-agent en lugar de curl/8.5.0.

8. Limpieza (importante: evita costes)

Cuando termines, deshaz el escenario:

  1. Consola EC2 → Instances → selecciona daw → Instance state → Terminate instance. Confirma. Terminate borra la instancia y su disco definitivamente; no basta con Stop (una máquina parada sigue pagando almacenamiento y al reanudarla la IP cambia).
  2. Network & Security → Security Groups: borra el security group creado. AWS borra la instancia pero no su security group.
  3. Network & Security → Key Pairs: opcionalmente borra daw-key.
  4. Borra daw-key.pem de tu máquina si ya no lo vas a usar.
  5. Opcional: quita la línea de /etc/hosts que añadiste.

Una instancia t2.small encendida genera cargo por hora. No la dejes corriendo entre clases.

9. Solución de problemas

Síntoma Causa probable Solución
El navegador dice que no encuentra planes.daw («no se puede resolver») Falta la línea en /etc/hosts o tiene una errata Revisa /etc/hosts; comprueba con ping planes.daw que resuelve a la IP de la instancia
curl -H "Host: …" funciona pero el navegador no El navegador no usa tu /etc/hosts (errata, o caché DNS) Corrige /etc/hosts; prueba en una ventana de incógnito
Ambos dominios muestran la misma web Un server_name mal escrito, un sitio sin activar (falta el enlace en sites-enabled) o el sitio default sigue activo Revisa server_name en cada fichero, ls -l /etc/Nginx/sites-enabled/ y que default esté borrado
http://<IP>/ muestra RunDash Es el servidor por defecto (primer server en orden alfabético) Comportamiento normal; ver apartado 7.4
puertos.daw da 403/404 El root apunta mal o el repositorio no está clonado Comprueba ls /var/www/puertos/index.html y el root del fichero puertos.daw
Las páginas de detalle de RunDash (/planes/...) dan 404 Falta $uri/index.html en el try_files Revisa el bloque location / del sitio planes.daw
curl devuelve Connection refused Nginx no está corriendo o no escucha en el 80 sudo systemctl status Nginx; si está parado, sudo Nginx -t para buscar errores
Nginx da 500 o no arranca tras un cambio de configuración Error de sintaxis o de permisos en el root sudo tail -n 30 /var/log/Nginx/error.log; lo primero que aparece suele ser la causa concreta (fichero y línea del error de sintaxis, o el errno del fallo de open()). Tras corregir, valida con sudo nginx -t y recarga con sudo systemctl reload Nginx.
Quiero saber qué peticiones han llegado realmente al servidor Necesitas verificar que Nginx recibe tráfico (y desde qué IP/ruta) sudo tail -f /var/log/Nginx/access.log mientras reproduces la petición en el navegador. Cada línea muestra IP, ruta pedida y código de respuesta. Si una ruta no aparece, es que ni siquiera ha llegado a Nginx (problema de DNS, hosts o security group).
E: Could not get lock /var/lib/dpkg/lock-frontend cloud-init sigue instalando/actualizando paquetes Espera 1-2 minutos y reintenta; vigílalo con cloud-init status
ssh: connect to host <IP> port 22: Connection timed out El security group no abre el puerto 22, o la IP cambió Revisa las reglas inbound y la IP pública actual
WARNING: UNPROTECTED PRIVATE KEY FILE! El .pem tiene permisos legibles por otros chmod 600 daw-key.pem