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:

Tecnologías y herramientas

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

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.

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_*;
}
Regresar