
Cada proyecto que despliegas tiene secretos. Contraseñas de base de datos, claves de API, tokens de autenticación. Y cuando gestionas varios servicios desde una plataforma como Dokploy, lo habitual es meter todo eso en las variables de entorno del panel y seguir adelante. Funciona, es rápido y parece suficiente.
Hasta que te paras a pensar en lo que eso implica. Esas variables están en texto plano en la base de datos de Dokploy. Cualquier persona con acceso al panel puede verlas. No hay registro de quién accedió a qué ni cuándo. Y si necesitas rotar una clave, tienes que ir servicio por servicio actualizando valores a mano.
Este artículo explica cómo montamos un gestor de secretos self-hosted con Infisical, integrado dentro de Dokploy como un servicio más, y cómo conectamos nuestras aplicaciones para que lean los secretos de forma segura sin tenerlos expuestos en variables de entorno.
No es que las variables de entorno sean intrínsecamente malas. Son el estándar para configurar contenedores Docker y la mayoría de plataformas PaaS las usan como mecanismo principal. El problema aparece cuando las usas para almacenar información sensible sin ninguna capa de protección adicional.
En un setup típico con Dokploy (o cualquier plataforma similar), los secretos quedan expuestos en varios puntos. En la base de datos interna de la plataforma, que los almacena sin cifrar. En la salida de docker inspect, que muestra todas las variables del contenedor. En el proceso dentro del contenedor, accesible a través de /proc/*/environ. Y en el propio panel de administración, visible para cualquier usuario con acceso.
Cuando tienes un solo proyecto con dos o tres variables, el riesgo es asumible. Pero cuando gestionas diez o quince servicios, cada uno con sus propias credenciales, la superficie de exposición crece y la gestión se vuelve un problema operativo.
Hay varias opciones para gestión de secretos. HashiCorp Vault es el referente de la industria, pero su complejidad operativa es considerable para un equipo pequeño o un desarrollador individual. AWS Secrets Manager y similares te atan a un proveedor cloud concreto. SOPS con age es elegante para cifrar ficheros en repositorios, pero no ofrece gestión centralizada ni audit log.
Infisical ocupa un punto intermedio que encaja bien en nuestro caso. Es open-source, se puede desplegar con un simple docker-compose, tiene una interfaz web completa para gestionar secretos por proyecto y entorno, ofrece cifrado AES-256-GCM en reposo, audit log de cada acceso y un CLI que permite inyectar secretos en contenedores sin modificar el código de la aplicación.
Y lo más práctico para nuestro contexto, se despliega como un servicio más dentro de Dokploy, comparte la misma red Docker que el resto de aplicaciones y no requiere infraestructura externa.
Infisical necesita tres componentes. Su propio backend (una aplicación Node.js), una base de datos PostgreSQL y una instancia de Redis. Los tres se empaquetan en un docker-compose que se despliega como un servicio independiente dentro de un proyecto de Dokploy.
La decisión de desplegarlo como un compose separado dentro del mismo proyecto de infraestructura (y no como parte de un stack existente) es deliberada. Infisical es la fuente de verdad para los secretos, así que su ciclo de vida debe ser independiente. Si necesitas redesplegar otro servicio del mismo proyecto, no quieres que Infisical se reinicie como efecto colateral.
services:
infisical:
image: infisical/infisical:v0.159.1
container_name: infisical-backend
restart: unless-stopped
environment:
- NODE_ENV=production
- ENCRYPTION_KEY=${ENCRYPTION_KEY}
- AUTH_SECRET=${AUTH_SECRET}
- DB_CONNECTION_URI=postgres://infisical:${DB_PASSWORD}@infisical-db:5432/infisical
- REDIS_URL=redis://infisical-redis:6379
- SITE_URL=${SITE_URL}
- TELEMETRY_ENABLED=false
depends_on:
infisical-db:
condition: service_healthy
infisical-redis:
condition: service_started
networks:
- dokploy-network
infisical-db:
image: postgres:17-alpine
container_name: infisical-db
restart: unless-stopped
environment:
- POSTGRES_USER=infisical
- POSTGRES_PASSWORD=${DB_PASSWORD}
- POSTGRES_DB=infisical
volumes:
- /etc/dokploy/infisical/postgres:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U infisical"]
interval: 10s
timeout: 5s
retries: 5
networks:
- dokploy-network
infisical-redis:
image: redis:8-alpine
container_name: infisical-redis
restart: unless-stopped
volumes:
- /etc/dokploy/infisical/redis:/data
networks:
- dokploy-network
networks:
dokploy-network:
external: trueEl compose conecta los tres servicios a dokploy-network, la red externa que Dokploy usa para todos sus contenedores. Esto permite que cualquier aplicación desplegada en Dokploy pueda comunicarse con Infisical internamente sin exponer puertos al exterior.
Las variables de entorno del compose (ENCRYPTION_KEY, AUTH_SECRET, DB_PASSWORD) son las únicas credenciales de bootstrap que necesita Infisical. Una vez arrancado, todos los demás secretos se gestionan desde su interfaz web.
El despliegue no fue completamente lineal. Hubo tres problemas que nos habrían ahorrado tiempo si los hubiéramos conocido de antemano. Como complemento, en la seguridad en la cadena de suministro de Docker cubrimos la otra cara de la moneda.
El formato de ENCRYPTION_KEY importa mucho. Infisical usa createCipheriv internamente, que requiere una clave AES con longitud exacta. Si generas la clave con openssl rand -base64 32 obtienes 44 caracteres que no son una longitud válida para AES. El formato correcto es openssl rand -hex 16, que produce 32 caracteres hexadecimales representando exactamente 16 bytes.
Las contraseñas con caracteres especiales rompen las URIs de conexión. Si tu password de base de datos contiene +, = o / (habitual en cadenas base64), al interpolarse dentro de una URI tipo postgres://user:password@host/db el resultado se corrompe. El + se interpreta como espacio y el parsing falla sin un mensaje de error claro. La solución es generar passwords estrictamente alfanuméricos para cualquier servicio cuya credencial vaya dentro de una URI.
Si una migración falla a medias, hay que limpiar antes de reintentar. Infisical ejecuta migraciones de base de datos en el primer arranque. Si una falla (como nos pasó por el problema de longitud de clave), la base de datos queda en un estado inconsistente. Redesplegar sin limpiar los datos de PostgreSQL no resuelve nada porque la tabla de control de migraciones cree que algunas ya se aplicaron. La solución es borrar los datos del volumen y dejar que arranque desde cero.
Una vez Infisical está corriendo, el flujo para conectar una aplicación tiene dos partes. La configuración en la interfaz de Infisical y la modificación del Dockerfile de la aplicación.
Primero creas un proyecto y añades los secretos en el entorno correspondiente (production, staging o development). Después creas una Machine Identity a nivel de organización, que es el equivalente a una cuenta de servicio. Le asignas autenticación Universal Auth, que genera un par de Client ID y Client Secret. Finalmente le das acceso al proyecto con rol de solo lectura.
La Machine Identity es lo que permite a la aplicación autenticarse contra Infisical de forma programática, sin intervención humana y con el mínimo privilegio necesario.
La aplicación necesita el CLI de Infisical instalado en la imagen Docker. El CLI se encarga de autenticarse, obtener los secretos y pasarlos como variables de entorno al proceso principal. La aplicación no sabe que Infisical existe, simplemente recibe las variables de entorno como siempre. Si te interesa la solución que adoptamos, en variables de entorno en scripts E2E lo explicamos con detalle.
# Instalar Infisical CLI en la etapa de runtime
RUN apk add --no-cache curl bash && \
curl -1sLf 'https://dl.cloudsmith.io/public/infisical/infisical-cli/setup.alpine.sh' | bash && \
apk add --no-cache infisical
# El CMD primero se autentica y después inyecta los secretos
CMD ["sh", "-c", "export INFISICAL_TOKEN=$(infisical login \
--method=universal-auth \
--client-id=$INFISICAL_UNIVERSAL_AUTH_CLIENT_ID \
--client-secret=$INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET \
--domain=$INFISICAL_API_URL --plain) && \
infisical run --token $INFISICAL_TOKEN \
--projectId $INFISICAL_PROJECT_ID \
--env prod --domain $INFISICAL_API_URL \
-- node server.js"]Un detalle importante es que infisical run no auto-autentica usando las variables de entorno del CLI, al menos en la versión actual. Hay que hacer un login explícito primero con infisical login --method=universal-auth, obtener el token y pasárselo a infisical run con el flag --token. Si intentas usar infisical run directamente, el CLI intenta abrir un navegador para login interactivo, lo cual falla dentro de un contenedor.
Donde antes la aplicación tenía diez o quince variables de entorno con secretos reales (claves de API, contraseñas, tokens), ahora tiene exactamente cuatro variables que apuntan a Infisical.
INFISICAL_UNIVERSAL_AUTH_CLIENT_ID=...
INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET=...
INFISICAL_PROJECT_ID=...
INFISICAL_API_URL=http://infisical-backend:8080La URL interna http://infisical-backend:8080 funciona porque ambos contenedores están en la misma red Docker. El tráfico nunca sale del servidor.
La pregunta legítima es si todo este montaje merece la pena frente a simplemente poner las variables en el panel de Dokploy y seguir adelante. La respuesta depende de tu contexto, pero estos son los beneficios concretos que obtienes.
Cifrado en reposo. Los secretos en Infisical están cifrados con AES-256-GCM. En las variables de entorno de Dokploy están en texto plano en una base de datos SQLite.
Audit log. Cada vez que una aplicación lee un secreto, queda registrado. Quién accedió, a qué proyecto, en qué entorno y cuándo. Con variables de entorno no tienes visibilidad de quién vio qué.
Revocación instantánea. Si sospechas que unas credenciales se han comprometido, revocas la Machine Identity con un clic y la aplicación pierde acceso inmediatamente. Con variables de entorno, tendrías que cambiar cada secreto individual en cada servicio afectado.
Gestión centralizada. Si necesitas rotar la contraseña de una base de datos compartida por tres servicios, la cambias en un sitio y los tres la recogen en el siguiente arranque. Sin Infisical, tienes que actualizar la variable en tres sitios distintos y redesplegar cada uno.
Mínimo privilegio. Cada aplicación tiene su propia Machine Identity con acceso de solo lectura a su proyecto específico. Si alguien obtiene las credenciales de Infisical de una aplicación, solo puede leer los secretos de ese proyecto, no los de toda la infraestructura.
Hay una paradoja evidente en este enfoque. Estamos usando un gestor de secretos para evitar tener secretos en las variables de entorno, pero las credenciales de acceso a Infisical siguen estando en las variables de entorno de Dokploy.
Es un punto válido, pero la diferencia es sustancial. Antes tenías N secretos reales expuestos directamente (claves de API, contraseñas de base de datos, tokens de servicios externos). Ahora tienes un único par de credenciales que no contiene ningún secreto real, que tiene acceso de solo lectura a un proyecto específico, que puede revocarse al instante y que deja rastro de cada uso en el audit log.
Siempre necesitas algún credential de bootstrap en algún sitio. La clave es que ese credential tenga el mínimo privilegio posible, sea revocable y deje trazabilidad. Mover los secretos reales a un sistema cifrado con control de acceso es una mejora significativa aunque el mecanismo de bootstrap sea una variable de entorno.
No es necesario migrar todas las aplicaciones de golpe. De hecho, recomendamos no hacerlo. El enfoque que mejor nos ha funcionado es elegir una aplicación sencilla como piloto, migrar sus secretos a Infisical, verificar que todo funciona correctamente durante unos días y después ir extendiendo al resto de servicios de forma progresiva.
En nuestro caso empezamos con un portfolio estático que solo tenía tres variables de entorno. Era lo bastante simple como para diagnosticar problemas rápidamente y lo bastante representativo como para validar el flujo completo.
Una vez confirmamos que la inyección de secretos funcionaba correctamente en desarrollo, staging y producción, migramos los servicios más críticos siguiendo el mismo patrón.
Desplegar un gestor de secretos self-hosted no es complejo si eliges la herramienta adecuada. Infisical con Docker Compose se levanta en quince minutos. Lo que lleva más tiempo es decidir la estrategia de migración, entender los matices del CLI y resolver los pequeños problemas de formato que solo aparecen cuando conectas piezas reales.
Si gestionas varios servicios en un VPS con Dokploy (o cualquier plataforma similar), tener un gestor de secretos centralizado te ahorra tiempo operativo a medio plazo y reduce significativamente la superficie de exposición de tus credenciales. No es una herramienta que necesites desde el primer día, pero es una de esas mejoras de infraestructura que, una vez puesta, te preguntas por qué no la instalaste antes. Si quieres profundizar, en la infraestructura con Dokploy lo cubrimos en detalle.
Los secretos en texto plano en variables de entorno son la norma en la mayoría de despliegues self-hosted. Funcionan hasta que dejan de funcionar, y cuando dejan de funcionar suele ser porque alguien vio algo que no debería haber visto. Un gestor de secretos no elimina todos los riesgos, pero convierte el acceso a información sensible en algo cifrado, auditable y revocable. Y eso es un salto cualitativo respecto a un campo de texto plano en un panel de administración.

Tres medidas concretas para proteger tu Dockerfile contra ataques de supply chain: verificación de checksums con SHA256, control de scripts npm con ignore-scripts y eliminación del package manager en la imagen de producción.
Los scripts E2E necesitan datos sensibles —tokens de API, credenciales, URLs privadas— sin que aparezcan en el código. En JMO Labs hemos añadido variables de script con modo privado: se inyectan automáticamente, se enmascaran en los logs y se acceden con una sintaxis limpia.

Cada decisión técnica tiene un porqué. Elegimos Next.js sobre Astro, SQLite sobre PostgreSQL y Docker con Dokploy sobre Vercel. Aquí explicamos las razones y lo que aprendimos por el camino.