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.dawy otro apuertos.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:
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:
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
curlpara comprobar el resultado. - Poder editar el fichero
/etc/hostsde tu máquina (necesita permisos de administrador).
3. Lanzar la instancia EC2
3.1. Abrir el asistente
- Entra en la consola de AWS → servicio EC2 → botón Launch instance.
3.2. Nombre y AMI
-
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. -
En Amazon Machine Image (AMI) busca y selecciona Ubuntu Server 24.04 LTS (x86_64).
- Una AMI es la imagen base del disco. Elegimos Ubuntu Server porque todo el software que necesitamos (Nginx, git, python3) está en sus repositorios.
- LTS (Long Term Support): Canonical la mantiene 5 años con actualizaciones de seguridad.
- x86_64/amd64: las instancias
t2son x86_64, así que la imagen debe ser de esa arquitectura (no ARM). - 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
- 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)
- En Key pairs pulsa Create new key pair:
- Key pair name:
daw-key - Key pair type: RSA
- Private key file format: .pem
- 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)
- 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 usamos0.0.0.0/0para que funcione desde cualquier red. - La regla de salida por defecto permite todo el tráfico saliente (necesario para
aptygit).
3.6. Disco
- 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
- Despliega Advanced details y en User data pega exactamente esto:
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
-
Pulsa Launch instance y espera los dos estados:
- Instance state: de
pendingarunning. - Status checks: las dos comprobaciones en verde (2/2).
- Instance state: de
-
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 600deja 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.pemindica la clave privada con la que autenticarte.ubuntu@<IP>: las imágenes de Ubuntu en AWS crean el usuarioubuntu(no se entra directo comoroot); luego usaremossudo.
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:
hostnamectl set-hostname daw fija el nombre de la máquina a daw.
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.
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:
- 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
aptse 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 concloud-init status(termina endone).
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):
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.
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:
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 siguientereloady empieza a servirlo. - Borras el enlace de
sites-enabled/→ Nginx deja de servirlo en el siguientereload, pero la config sigue ensites-availablelista para reactivarla con un sololn -s.
Este mecanismo se conecta con Nginx.conf mediante una única línea al final del bloque
http { ... }:
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ónavailable/enabledes 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.confes 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
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_filescon$uri/index.html? Porque Astro (RunDash) genera las rutas como directorios con unindex.htmldentro:/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
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 mismotry_filessirve 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 destinocrea un enlace simbólico (un acceso directo): el fichero real vive ensites-availabley Nginx lo lee a través del enlace ensites-enabled.- Borramos
defaultporque está marcado comodefault_serveren el puerto 80 y sirve/var/www/html: si lo dejamos, respondería él a las peticiones que no coincidan con ningúnserver_name.
Valida la sintaxis de toda la configuración antes de aplicarla:
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:
reloadle 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 derestart).enablehace que Nginx arranque solo al encender la máquina.
Comprueba que Nginx escucha en el 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):
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:
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:
| 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 enaccess.logbuscando el código 404: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:
- 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):
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:
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 pide solo las cabeceras (método HEAD). Debes obtener HTTP/1.1 200 OK y
Server: Nginx. Repite con el otro sitio:
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?
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:
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.xo192.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 altry_files $uri/index.htmlse sirve desde/var/www/planes/planes/5k/index.html). - Si una ruta no existiera, verías
404en lugar de200y, además, una línea correspondiente enerror.log.
Para salir de tail -f: Ctrl+C.
Si quieres ver también el
user-agentreal del navegador (no el decurl), abrehttp://planes.dawen el navegador y observa cómo aparece una línea conMozilla/5.0 ...como user-agent en lugar decurl/8.5.0.
8. Limpieza (importante: evita costes)
Cuando termines, deshaz el escenario:
- 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). - Network & Security → Security Groups: borra el security group creado. AWS borra la instancia pero no su security group.
- Network & Security → Key Pairs: opcionalmente borra
daw-key. - Borra
daw-key.pemde tu máquina si ya no lo vas a usar. - Opcional: quita la línea de
/etc/hostsque añadiste.
Una instancia
t2.smallencendida 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 |