Jair Rivera · Blog

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 9443 de Portainer.
  • Separación clara entre configuración global de Hestia y configuración específica del dominio.
  • Evitar que la carpeta public_html del 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_html vacío es clave para que el reverse proxy tome control.
  • Un nginx.conf limpio y simple es mejor que mezclarlo con reglas para HTML estático.
  • Probar siempre con nginx -t antes 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_*;
}