Qué vas a conseguir
Un servidor Apache en Ubuntu que sirve varios sitios propios (no como proxy, sino con sus ficheros), cada uno con su nombre, su certificado de Let's Encrypt renovado solo, la redirección a HTTPS, las cabeceras de seguridad y un log que dice cuánto tarda cada petición. Y, cuando alguien abre su web y ve Apache2 Default Page: It works! o la web de otro departamento, saber leer apache2ctl -S y encontrar el motivo.
Antes de empezar
- Ubuntu Server 24.04 LTS con
apache2instalado (sudo apt install apache2) y un usuario consudo. - Los nombres públicos en DNS apuntando al servidor: aquí
reservas.example.comyportal.example.com, con suswww. - Los puertos 80 y 443 abiertos desde internet: Let's Encrypt valida por el 80 (
sudo ufw allow "Apache Full"). - Para nombres internos (
*.contoso.local), Let's Encrypt no sirve: el certificado lo emite la CA corporativa y se configura a mano como en la guía de proxy inverso.
1. Cómo organiza Apache los sitios en Ubuntu
| Ruta | Qué contiene |
|---|---|
/etc/apache2/apache2.conf | La configuración global y los LogFormat |
/etc/apache2/sites-available/ | Un fichero .conf por sitio, activo o no |
/etc/apache2/sites-enabled/ | Enlaces a los sitios activos, creados por a2ensite |
/etc/apache2/mods-enabled/ | Módulos activos, con a2enmod / a2dismod |
/etc/apache2/conf-enabled/ | Fragmentos globales, con a2enconf (por ejemplo security.conf) |
/var/www/ | El contenido de cada sitio |
/var/log/apache2/ | access.log, error.log y los logs propios de cada sitio |
Los ficheros de sites-enabled se cargan por orden alfabético, y ese orden importa: el primero que escucha en un puerto es el sitio por defecto de ese puerto. El que trae Ubuntu se llama 000-default.conf precisamente para ir el primero.
2. Un virtual host por sitio
<VirtualHost *:80> ServerName reservas.example.com ServerAlias www.reservas.example.com DocumentRoot /var/www/reservas <Directory /var/www/reservas> Options -Indexes AllowOverride None Require all granted </Directory> ErrorLog ${APACHE_LOG_DIR}/reservas_error.log CustomLog ${APACHE_LOG_DIR}/reservas_access.log tiempos</VirtualHost>sudo mkdir -p /var/www/reservas && sudo chown -R www-data:www-data /var/www/reservassudo a2ensite reservassudo a2dissite 000-defaultsudo apache2ctl configtestSyntax OKsudo systemctl reload apache2- ServerName es el nombre principal; ServerAlias, los demás (con
wwwy sin él). Un nombre que no está en ninguno de los dos no es de este sitio. Options -Indexesevita que una carpeta sinindex.htmlmuestre la lista de ficheros.a2ensiteya2dissitesolo crean y quitan el enlace: hace faltareloaddespués. El formatotiemposse define en el apartado 5; hasta entonces,configtestdará error.
3. HTTPS con certbot y su renovación
sudo apt install certbot python3-certbot-apachesudo a2enmod ssl headers rewritesudo certbot --apache -d reservas.example.com -d www.reservas.example.com --redirect -m it@example.com --agree-tosSuccessfully received certificate.Certificate is saved at: /etc/letsencrypt/live/reservas.example.com/fullchain.pemKey is saved at: /etc/letsencrypt/live/reservas.example.com/privkey.pemSuccessfully deployed certificate for reservas.example.com to /etc/apache2/sites-available/reservas-le-ssl.confCongratulations! You have successfully enabled HTTPS on https://reservas.example.com and https://www.reservas.example.com- certbot crea un segundo fichero,
reservas-le-ssl.conf, con el<VirtualHost *:443>y las rutas del certificado. Lo que cambies después (cabeceras, logs) va en los dos ficheros: el del 80 y el del 443. --redirectañade al sitio del puerto 80 una regla que manda todo a HTTPS con un 301. Sin certbot, lo mismo en una línea:Redirect permanent / https://reservas.example.com/.- La renovación: el paquete de Ubuntu instala
certbot.timer, que comprueba dos veces al día y renueva lo que caduca en menos de 30 días. Con el snap de certbot, el temporizador essnap.certbot.renew.timer. Let's Encrypt dejó de enviar correos de aviso de caducidad en 2025: si la renovación falla, nadie te avisa salvo tu monitorización.
systemctl list-timers certbot.timersudo certbot certificatessudo certbot renew --dry-runCongratulations, all simulated renewals succeeded: /etc/letsencrypt/live/reservas.example.com/fullchain.pem (success)4. Cabeceras de seguridad y lo que Apache cuenta de sí mismo
En el <VirtualHost *:443> del sitio (necesita mod_headers):
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"Header always set X-Content-Type-Options "nosniff"Header always set X-Frame-Options "SAMEORIGIN"Header always set Referrer-Policy "strict-origin-when-cross-origin"Y en /etc/apache2/conf-available/security.conf, que Ubuntu trae activo, para que los errores no digan la versión exacta de Apache y del sistema:
ServerTokens ProdServerSignature OffTraceEnable Off- HSTS obliga al navegador a usar HTTPS durante un año. Actívalo cuando HTTPS funcione bien en todos los nombres: con
includeSubDomains, también en los subdominios. Empieza con unmax-agecorto si tienes dudas. - Una Content-Security-Policy protege más que todas las anteriores, pero depende de cada aplicación: se acuerda con quien la desarrolla y se prueba antes con
Content-Security-Policy-Report-Only.
curl -sI https://reservas.example.com | grep -iE "^(server|strict|x-content|x-frame|referrer)"Server: ApacheStrict-Transport-Security: max-age=31536000; includeSubDomainsX-Content-Type-Options: nosniffX-Frame-Options: SAMEORIGINReferrer-Policy: strict-origin-when-cross-origin5. Formatos de log
apache2.conf ya define combined, common y vhost_combined. Al formato combinado le falta lo más útil para diagnosticar: el nombre del sitio que respondió y el tiempo. Un formato propio en /etc/apache2/conf-available/log-tiempos.conf, activado con a2enconf log-tiempos:
LogFormat "%v %h %l %u %t \"%r\" %>s %O \"%{Referer}i\" \"%{User-Agent}i\" %D" tiempos%ves elServerNamedel virtual host que atendió la petición: con él, el ticket del apartado 6 se ve en el log.%Des el tiempo en microsegundos;%>s, el código final;%O, los bytes enviados.- Si delante hay un balanceador,
%hserá siempre su IP: la del usuario va en%{X-Forwarded-For}i, o conmod_remoteipconfigurado. - Los logs de
/var/log/apache2/ya los rota/etc/logrotate.d/apache2(14 días, comprimidos).
6. El ticket: "la web muestra la página por defecto de Apache, o la de otro sitio"
Martes, 10:15. Marketing ha publicado portal.example.com en el mismo servidor. Desde fuera, http://www.portal.example.com muestra Apache2 Ubuntu Default Page y https://portal.example.com enseña la web de reservas, con un aviso de certificado.
Apache elige el virtual host en dos pasos: primero por IP y puerto, y entre los que coinciden, por el nombre pedido. Si ningún ServerName ni ServerAlias coincide, responde el primero de ese puerto. apache2ctl -S enseña exactamente esa tabla:
sudo apache2ctl -SVirtualHost configuration:*:443 is a NameVirtualHost default server reservas.example.com (/etc/apache2/sites-enabled/reservas-le-ssl.conf:2) port 443 namevhost reservas.example.com (/etc/apache2/sites-enabled/reservas-le-ssl.conf:2) alias www.reservas.example.com*:80 is a NameVirtualHost default server srv-web-01.contoso.local (/etc/apache2/sites-enabled/000-default.conf:1) port 80 namevhost srv-web-01.contoso.local (/etc/apache2/sites-enabled/000-default.conf:1) port 80 namevhost portal.example.com (/etc/apache2/sites-enabled/portal.conf:1) port 80 namevhost reservas.example.com (/etc/apache2/sites-enabled/reservas.conf:1) alias www.reservas.example.comCómo se lee, de más a menos frecuente:
000-defaultsigue activo y es eldefault serverdel 80. A él va todo lo que no coincide: aquí,www.portal.example.com, porqueportal.confno tieneServerAlias. Solución: añadir el alias ya2dissite 000-default(o dejarlo, pero con una página neutra, nunca la de Apache).- Falta el sitio del 443:
portal.example.comsolo aparece en el 80. En el 443 no hay nadie con ese nombre, así que responde el primero (reservas) con su certificado, y el navegador avisa de que el nombre no coincide. Solución:certbot --apache -d portal.example.com -d www.portal.example.com. - Un
VirtualHostcon IP (<VirtualHost 10.20.5.10:443>) junto a otros con*:443: el de la IP gana siempre para esa dirección, aunque el nombre no coincida. Todos con*o todos con IP, no mezclados. - El sitio no está activo: el fichero está en
sites-availablepero no aparece en-S. Faltaa2ensiteyreload. - El DNS apunta a otro servidor, o el navegador recuerda un 301 antiguo:
curl -sI -H "Host: portal.example.com" http://127.0.0.1/desde el propio servidor dice qué responde Apache, sin DNS ni caché de por medio.
sudo apache2ctl configtest && sudo systemctl reload apache2curl -sI https://www.portal.example.com | head -n 1HTTP/1.1 200 OKsudo tail -n 1 /var/log/apache2/portal_access.logportal.example.com 203.0.113.24 - - [03/Nov/2026:10:31:07 +0100] "GET / HTTP/1.1" 200 5120 "-" "Mozilla/5.0" 1840Una costumbre que evita este ticket: un primer sitio propio (000-catchall.conf) que devuelve un 404 o una página neutra para cualquier nombre desconocido, en el 80 y en el 443. Así un error de configuración nunca enseña la web de otro.
En el ticket: la salida de apache2ctl -S antes y después, qué faltaba (alias, sitio del 443, a2ensite) y la comprobación con curl.
Siguientes pasos
- La primera guía de este tema, Nginx y Apache como proxy inverso, publica una aplicación interna con TLS corporativo y resuelve el 502.
- La guía de Ubuntu Server: los primeros 30 minutos deja el servidor con SSH, ufw y parches antes de instalar Apache.
- Si el sitio es un WordPress, la guía de WordPress en Docker cubre copias y bastionado.
¿Falta algo o en tu entorno es distinto? Escríbeme y lo corrijo.
Tech Career Lab · newsletter
¿Te ha servido? Cada dos semanas envío una idea práctica para perfiles IT: LinkedIn, proyectos, networking y procesos de selección.
Apúntate y llévate las 33 ideas →