Portainer — Dominio, SSL y Reverse Proxy en HestiaCP
Este documento describe cómo se configuró Portainer para ser accesible mediante un
dominio propio con SSL usando HestiaCP y Nginx como
reverse proxy, en lugar de acceder directamente por IP:puerto. Se documentan los errores,
la causa raíz, los archivos involucrados y la solución final que no afecta a otros sitios alojados en el
mismo servidor.
Objetivo del proyecto
El objetivo fue exponer la interfaz de Portainer a través de un subdominio con SSL, por ejemplo:
https://portainer.dominio.com
en lugar de usar directamente la IP del servidor y el puerto:
https://[IP_DEL_SERVIDOR]:9443
De esta forma se consigue:
- Una URL legible y fácil de recordar.
- Certificado SSL válido (Let's Encrypt) gestionado por HestiaCP.
- Exposición controlada de Portainer sin comprometer otros sitios.
Tecnologías y herramientas
- Portainer CE — panel de administración para Docker.
- Docker Engine — motor de contenedores donde corre Portainer.
- HestiaCP — panel de control que gestiona Nginx, dominios y SSL.
- Nginx — servidor web y reverse proxy.
- Let's Encrypt — certificados SSL gratuitos integrados en HestiaCP.
Estructura del proyecto (rutas clave en el servidor)
Para que el reverse proxy funcione correctamente en HestiaCP, es importante conocer las rutas implicadas en el dominio usado para Portainer:
/home/USUARIO/conf/web/portainer.dominio.com/
├── nginx.conf # Configuración Nginx específica del dominio (aquí va el proxy)
├── nginx.ssl.conf # Configuración SSL autogenerada por Hestia (no se modifica)
├── nginx.forcessl.conf # Regla para forzar HTTPS (incluida por nginx.conf)
└── ... # Otros archivos internos de Hestia
/home/USUARIO/web/portainer.dominio.com/
├── public_html/ # Carpeta pública del dominio (se deja vacía para que Nginx no sirva HTML)
└── document_errors/ # Páginas de error manejadas por Hestia
El error inicial vino, en parte, por editar o depender de plantillas globales en lugar de usar
nginx.conf específico del dominio:
# Archivo global que NO se debe tocar para este caso:
#/usr/local/hestia/data/templates/web/nginx/default.tpl
Principales funcionalidades logradas
- Acceso a Portainer mediante un subdominio con SSL válido.
- Uso de Nginx como reverse proxy hacia el puerto interno
9443de Portainer. - Separación clara entre configuración global de Hestia y configuración específica del dominio.
- Evitar que la carpeta
public_htmldel dominio interfiera mostrando HTML estático. - Conservar la compatibilidad con otros dominios alojados en el mismo servidor.
Diseño de la arquitectura (flujo de peticiones)
A nivel de arquitectura, el flujo de una petición hacia Portainer quedó así:
Navegador → https://portainer.dominio.com
↓
HestiaCP (Nginx con SSL Let's Encrypt)
↓
nginx.conf del dominio # /home/USUARIO/conf/web/portainer.dominio.com/nginx.conf
↓
Reverse Proxy (proxy_pass) → https://127.0.0.1:9443
↓
Contenedor de Portainer (Docker)
Es importante que public_html esté vacío, para que Nginx no sirva una página HTML de “Under Construction” en vez de reenviar la petición a Portainer.
Resultados y aprendizajes
A lo largo del proceso surgieron varios errores (pantallas 403, páginas de “Under Construction”, reinicios fallidos de Nginx), que permitieron entender mejor cómo HestiaCP organiza sus archivos.
- Los archivos globales de templates de Hestia no deben usarse para un caso puntual como Portainer.
- La configuración correcta vive en
/home/USUARIO/conf/web/DOMINIO/nginx.conf. - Dejar
public_htmlvacío es clave para que el reverse proxy tome control. - Un
nginx.conflimpio y simple es mejor que mezclarlo con reglas para HTML estático. - Probar siempre con
nginx -tantes de reiniciar el servicio.
A continuación, se deja como referencia el bloque final usado para el proxy inverso:
server {
listen [IP_DEL_SERVIDOR]:80;
server_name portainer.dominio.com;
include /home/USUARIO/conf/web/portainer.dominio.com/nginx.forcessl.conf*;
location / {
proxy_pass https://127.0.0.1:9443;
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 https;
proxy_ssl_verify off;
}
location /error/ {
alias /home/USUARIO/web/portainer.dominio.com/document_errors/;
}
include /home/USUARIO/conf/web/portainer.dominio.com/nginx.conf_*;
}