Docker Secrets: cómo gestionar credenciales en Docker Compose paso a paso

Docker Secrets: cómo gestionar credenciales en Docker Compose paso a paso

⚡ Nivel: Intermedio | ⏱ Tiempo: 11 min | 🖥 Sistema: Docker Compose / Swarm

Índice

  1. El problema: por qué tus variables de entorno no son tan seguras
  2. Qué son Docker Secrets y cómo funcionan
  3. Compose standalone vs Swarm: la diferencia que nadie te explica
  4. Sintaxis básica en tu compose.yml
  5. Ejemplo completo: API + base de datos con secrets
  6. Qué es Docker Swarm y para qué sirve
  7. Cómo usar Docker Secrets externos con Swarm
  8. Cómo leer el secret dentro de tu app
  9. Buenas prácticas para gestionar Docker Secrets de forma segura
  10. Docker Secrets en producción: cuándo es suficiente y cuándo no
  11. Preguntas frecuentes sobre Docker Secrets

El problema: por qué tus variables de entorno no son tan seguras

Escribes esto en tu compose.yml y sigues con tu vida:

environment:
  POSTGRES_PASSWORD: MiPassword123

Funciona. Pero acabas de dejar esa contraseña en tres sitios que probablemente no controlas:

  • docker inspect — cualquiera con acceso al Docker socket puede leerla en texto plano:
  docker inspect mi_contenedor | grep -A 5 Env
  • El historial de tu shell y tus logs — si la app imprime sus variables al arrancar (muchas lo hacen para debug), ahí queda.
  • Git — si el compose.yml con la contraseña real acaba commiteado, ya no importa cuánto la rotes: sigue en el historial.

Y si la metes directamente en el Dockerfile con ENV o ARG, es peor todavía: puede quedar expuesta en las capas o metadatos de la imagen, incluso aunque elimines posteriormente la instrucción del Dockerfile. Ya hablé de este error específico en la guía de cómo optimizar un Dockerfile.

Docker Secrets existe justo para sacar las credenciales de en medio de todo esto.

Qué son Docker Secrets y cómo funcionan

Los Docker Secrets son un mecanismo de Docker para almacenar y proporcionar información sensible a los contenedores sin tener que meterla directamente en el docker-compose.yml, Dockerfile o variables de entorno.

En vez de inyectar el valor como variable de entorno, Docker monta el secreto como un archivo de solo lectura dentro del contenedor, típicamente en /run/secrets/.

La diferencia es sutil pero importante:

  • ✅ El valor del secret no aparece en docker inspect
  • ✅ No aparece en docker logs a menos que tu app lo imprima explícitamente
  • ✅ No queda horneado en las capas de la imagen
  • ✅ Solo lo ven los servicios que lo declaran explícitamente

Mecánicamente es un mount más — parecido a los bind mounts que ya viste en la guía de Docker Volumes, solo que pensado específicamente para datos sensibles y con restricciones de acceso extra.

Docker Secrets vs .env: ¿cuál utilizar?

Los archivos .env son útiles para configurar variables de entorno en Docker Compose, pero no son un mecanismo específico para gestionar credenciales.

Si necesitas utilizar contraseñas, tokens o API keys en tiempo de ejecución, Docker Secrets permite montar esos valores como archivos dentro del contenedor en /run/secrets/.

Método Uso
environment: Variables de configuración
.env Configuración de Docker Compose
Docker Secrets Credenciales y datos sensibles en tiempo de ejecución
BuildKit Secrets Secretos necesarios durante la construcción de una imagen
Swarm Secrets Secretos gestionados por Docker Swarm

¿Docker Secrets es mejor que .env?

No son exactamente alternativas equivalentes. .env es principalmente un mecanismo de configuración de Docker Compose, mientras que Docker Secrets está pensado para gestionar datos sensibles que los servicios necesitan durante la ejecución.

Si necesitas guardar una contraseña, token o API key que debe estar disponible dentro de un contenedor, Docker Secrets permite montarla como archivo en /run/secrets/. Docker Compose concede acceso al secret únicamente a los servicios que lo declaran explícitamente.

Compose standalone vs Swarm: la diferencia que nadie te explica

Aquí es donde la mayoría de tutoriales se quedan cortos. "Docker Secrets" no es una sola cosa — se comporta distinto según dónde lo uses:

Compose Swarm
Secret desde archivo ✅ ✅
Cifrado en reposo gestionado por Docker ❌ ✅
Cifrado en tránsito entre nodos No aplica ✅
Rotación centralizada Manual Gestionada por el orquestador
Requiere docker swarm init No Sí
Soporta external: true Solo para referenciar (limitado) Sí, nativo

El matiz importante: si estás en un VPS con docker compose up (sin Swarm) — el caso típico de un self-hosted en Hostinger o similar — los secrets no están cifrados en disco. Lo que ganas es que dejan de aparecer en docker inspect, en logs y en las capas de la imagen. Eso ya es una mejora real frente a variables de entorno, pero no es lo mismo que el cifrado en reposo que ofrece Swarm. La seguridad real de tus secrets en modo standalone depende de los permisos del filesystem y de tus backups.

Sintaxis básica en tu compose.yml

Lo mínimo que necesitas:

  • Un archivo con el valor

  • Dos bloques en el compose.yml.

mkdir -p secrets
echo "MiPasswordSeguro123" > secrets/db_password.txt
chmod 600 secrets/db_password.txt
echo "secrets/" >> .gitignore
services:
  db:
    image: postgres:16
    secrets:
      - db_password
    environment:
      POSTGRES_PASSWORD_FILE: /run/secrets/db_password

secrets:
  db_password:
    file: ./secrets/db_password.txt

Fíjate en POSTGRES_PASSWORD_FILE en vez de POSTGRES_PASSWORD.

Algunas imágenes oficiales, como PostgreSQL, MySQL, MariaDB o RabbitMQ, implementan una convención _FILE en su entrypoint para leer automáticamente el secreto desde /run/secrets/.

el entrypoint de la imagen lee el archivo por ti y arranca con esa contraseña, sin que tengas que tocar nada más.

Ejemplo completo: API + base de datos con secrets

Un caso real donde usaremos POSTGRES_PASSWORD_FILE en lugar de POSTGRES_PASSWORD. El ejemplo utilizará una contraseña de base de datos y una API key para un servicio externo:

proyecto/
├── compose.yml
├── .gitignore
└── secrets/
    ├── db_password.txt
    └── api_key.txt
services:
  api:
    image: node:20-alpine
    secrets:
      - db_password
      - api_key
    environment:
      DB_PASSWORD_FILE: /run/secrets/db_password
      API_KEY_FILE: /run/secrets/api_key
    depends_on:
      - db

  db:
    image: postgres:16
    secrets:
      - db_password
    environment:
      POSTGRES_PASSWORD_FILE: /run/secrets/db_password
    volumes:
      - db_data:/var/lib/postgresql/data

secrets:
  db_password:
    file: ./secrets/db_password.txt
  api_key:
    file: ./secrets/api_key.txt

volumes:
  db_data:

Levántalo y comprueba la diferencia:

Ahora dentro de nuestra carpeta proyecto, ejecutamos:

docker compose up -d

A continuación ejecutaremos el siguiente comando para comprobar que dentro del contenedor, el secret está disponible como archivo:

docker compose exec db cat /run/secrets/db_password
  • docker compose exec db → te mete dentro del contenedor db, como si hicieras SSH a él.
  • cat /run/secrets/db_password → lee el archivo que Docker montó ahí con el valor del secret.

Por lo contrario veremos que no aparece la contraseña expuesta en las variables de entorno:

docker inspect $(docker compose ps -q db) | grep -A 10 '"Env"'
💡
Vas a ver POSTGRES_PASSWORD_FILE=/run/secrets/db_password en el inspect — la ruta, no la contraseña. Eso es exactamente lo que buscamos.

Qué es Docker Swarm y para qué sirve

💡
Si estás utilizando docker compose up en un servidor individual, puedes saltarte esta sección. Docker Compose permite utilizar secrets sin necesidad de configurar Docker Swarm. Swarm es relevante cuando quieres utilizar su sistema de gestión de secrets y desplegar servicios mediante docker stack deploy.

Docker Swarm es el coordinador de contenedores que ya trae integrado el propio Docker Engine.

Es una funcionalidad que se activa con el comando:

docker swarm init

Docker Swarm permite convertir uno o varios servidores Docker en un clúster que puede gestionarse como una única infraestructura de orquestación.

docker-swarm-explicacion.png

¿Qué gana tu infraestructura al activarlo?

  • Nodos manager y worker: Los managers deciden dónde y como se ejecuta cada cosa y los workers solo ejecutan los contenedores.
  • Servicios en vez de contenedores sueltos: Imagina que declaras " necesito 3 réplicas de esta imagen corriendo siempre" y Swarm se encarga de mantenerlas vivas,reinicia las que se cuelgen y repartiendolas entre los nodos disponibles.
  • **Actualizaciones sin downtime:**docker service update puede desplegar una nueva versión de la imagen contenedor a contenedor, con rollback automático si algo falla.
  • Red overlay cifrada entre nodos: Si tienes varios servidores, se comunican entre sí de forma cifrada sin que tengas que montar una VPN aparte.
  • Y lo que nos interesa aquí: gestión de secrets con cifrado real. Los managers guardan el estado del clúster (incluidos los secrets) en un registro distribuido llamado raft log, cifrado en disco. Esa es la razón mecánica por la que en la tabla comparativa de antes decíamos que Swarm sí cifra en reposo y Compose standalone no — no es una promesa de marketing, es literalmente dónde y cómo se almacena el dato.

swarm_beneficios_radial.svg

¿Es esto lo mismo que Kubernetes?

No exactamente. Swarm y Kubernetes son plataformas de orquestación con características y niveles de complejidad diferentes. Swarm puede utilizarse tanto en un único nodo como en un clúster de varios nodos.

Kubernetes ofrece más potencia y flexibilidad, pero también requiere una mayor complejidad operativa. Para utilizar Docker Secrets con Compose no necesitas utilizar ninguna de estas plataformas.

Podemos activarlo con el comando:

docker swarm init

Si tu servidor tiene varias interfaces de red y quieres forzar cuál usará Swarm para comunicarse entre nodos:

docker swarm init --advertise-addr <IP_DE_TU_SERVIDOR>

Ahora comprueba que está todo en orden con:

docker info | grep Swarm 
  docker node ls

   # tu nodo aparece como Leader

Cómo usar Docker Secrets externos con Swarm

Con Swarm activo, ya puedes crear secrets gestionados de forma centralizada.

Esto es útil cuando varios stack necesitan compartir la misma credencial sin que cada proyecto tenga su propia copia del archivo.

docker secret create db_password secrets/db_password.txt
docker secret ls

También puedes crear el secret sin pasar por un archivo,directamente desde stdin.

Esto es más cómodo si usamos scripts de despliegue que generan la contraseña en el momento.

echo "MiPasswordSeguro123" | docker secret create db_password -

Una propiedad de seguridad que vale la pena remarcar

El comando docker secret inspect db_password te ofrece metadatos como:

  • Fecha de creación
  • Tamaño
  • Que servicios lo usan

Pero nunca te dará el valor. Una vez creado, el valor del secret es de solo escritura.

Ni siquiera con acceso de administrador puedes volver a leerlo desde la CLI de gestión.

Captura de pantalla 2026-09-04 184242.png

Lo referencias en el archivo compose.yml como external,sin volver a escribir el valor en ningún archivo del proyecto:

services:
  db:
    image: postgres:16
    secrets:
      - db_password
    environment:
      POSTGRES_PASSWORD_FILE: /run/secrets/db_password

secrets:
  db_password:
    external: true

Ahora lo despliegas con el comando docker stack deploy no con docker compose up.

docker stack deploy -c compose.yml mi_stack

Comprobamos que todo salió bien

docker stack services mi_stack

La trampa de la inmutabilidad

Aquí es donde la mayoría se bloquea la primera vez:

  • Un docker secret no se puede editar una vez creado
  • Si quieres cambiar la contraseña, no existe un "docker secret update"

El flujo correcto sería:

1. Crear una nueva versión con un nombre distinto:

Primero crearemos el archivo nuevo que contendrá la nueva contraseña.

nano secrets/db_password_nuevo.txt

   docker secret create db_password_v2 secrets/db_password_nuevo.txt
2.Actualizar el servicio para que apunte a la nueva versión
   docker service update \
     --secret-rm db_password \
     --secret-add source=db_password_v2,target=db_password \
     mi_stack_db
3.Una vez confirmado que el servicio arrancó bien con la nueva versión, eliminar la antigua
   docker secret rm db_password

Este esquema de versionado (db_password, db_password_v2, db_password_v3...) es el patrón estándar para rotar secrets en Swarm. incómodo comparado con simplemente sobreescribir un archivo, pero es precisamente lo que garantiza que nunca haya una ventana en la que el secret "desaparezca" a mitad de un despliegue.

Diferencias que debes tener en cuenta al pasar de docker compose up a docker stack deploy

Algunas claves de tu compose.yml se comportan distinto o se ignoran direcamente en el modo stack.

Clave En docker compose up En docker stack deploy
depends_on Respeta el orden de arranque Se ignora — Swarm no garantiza orden entre servicios
build: Construye la imagen Se ignora — necesitas la imagen ya publicada en un registro
restart: Usa la política tal cual (unless-stopped, etc.) Se traduce a deploy.restart_policy, con sintaxis propia
Redes Bridge por defecto Overlay cifrada entre nodos

Si tu objetivo al usar Swarm es tener secrets con cifrado en reposo conviene que sepas que estás cambiando también la forma de desplegar, no solo la gestión de credenciales.

Cómo leer el secret dentro de tu app

Esta es una parte donde es fácil equivocarnos y frustrarnos por tanto vamos a ir despacio y usaremos un ejemplo real, el mismo que usamos anteriormente, API + base de datos.

Cuando escribimos lo siguiente en el compose.yml

environment:
  DB_PASSWORD_FILE: /run/secrets/db_password

Es fácil creer que tu aplicación ahora tiene la contraseña disponible mágicamente. Lo que está ocurriendo realmente es que le estás dando a tu app una variable de entorno cuyo valor es una ruta de archivo

/run/secrets/db_password

No la contraseña en sí.Hay 2 pasos:

  1. Docker monta el archivo con el valor real en esa ruta, dentro del contenedor.
  2. Algo dentro del contenedor tiene que leer ese archivo para obtener el valor.

El segundo paso no ocurre solo.Quién hace ese paso numero dos depende de qué imagen estés usando.

Tendremos dos escenarios completamente distintos para entenderlo

Escenario A: Imágenes oficiales con soporte nativo

Postgres,MySQL,MariaDB...,ya traen dentro de su propio script de arranque (entrypoint),una lógica parecida a esta (simplificada):

# Esto es lo que hace, a grandes rasgos, el entrypoint de la imagen oficial

if [ -n "$POSTGRES_PASSWORD_FILE" ]; then
  POSTGRES_PASSWORD=$(cat "$POSTGRES_PASSWORD_FILE")
fi

Por eso con postgres:16 todo "simplemente funciona": la propia imagen busca si existe una variable _FILE, lee el archivo, y arranca Postgres con ese valor. Tú no escribes ni una línea de código para eso.

Escenario B — tu propia app, o cualquier imagen genérica

Tu propia app puede estar desarrollada, por ejemplo, en Node.js, Python o Go, o utilizar tu propio Dockerfile. Aquí no hay ningún entrypoint mágico esperando variables _FILE, porque ese comportamiento depende de la imagen o de la aplicación. Si no escribes el código que lee el archivo, simplemente no pasa nada: tu app arranca sin obtener el valor del secret.

Ejemplo completo, paso a paso

Crea esta estructura para entenderlo mejor con un ejemplo:

proyecto/
├── compose.yml
├── secrets/
│   └── mi_secreto.txt
└── app/
    └── leer-secret.js
mkdir -p secrets app

echo "valor-secreto-123" > secrets/mi_secreto.txt

crea el archivo compose.yml

services:
  app:
    image: node:20-alpine
    working_dir: /app
    volumes:
      - ./app:/app
    command: node leer-secret.js
    secrets:
      - mi_secreto
    environment:
      MI_SECRETO_FILE: /run/secrets/mi_secreto

secrets:
  mi_secreto:
    file: ./secrets/mi_secreto.txt

node:20-alpine es una imagen genérica — no sabe nada de MI_SECRETO_FILE. Así que en app/leer-secret.js implementamos nosotros mismos el escenario B, paso por paso:

Escribe este contenido en leer-secret.js

const fs = require('fs');

// Paso 1: leemos la variable de entorno -> obtenemos una RUTA (string)

const ruta = process.env.MI_SECRETO_FILE;
console.log('Paso 1 — la variable de entorno contiene una ruta:', ruta);



// Paso 2: usamos esa ruta para leer el archivo -> ahí está el valor real


const valor = fs.readFileSync(ruta, 'utf8').trim();
console.log('Paso 2 — el contenido de ese archivo es:', valor);

Levántalo:

docker compose up

Verás en la terminal, exactamente de los dos pasos de los que hablamos antes

Paso 1 — la variable de entorno contiene una ruta: /run/secrets/mi_secreto


Paso 2 — el contenido de ese archivo es: valor-secreto-123

Captura de pantalla 2026-09-16 232522.png

Docker monta un archivo en una ruta conocida y tu código decide qué hacer con él.

Qué pasa si te saltas este paso (el error típico)

Si por error o costumbre escribes esto en leer-secret.js pensando que va a funcionar igual que con Postgres

// ❌ Esto NO funciona con una imagen genérica

const valor = process.env.MI_SECRETO;
console.log(valor); // → undefined

Captura de pantalla 2026-09-16 233220.png

Te vas a encontrar con undefined, porque nunca declaraste MI_SECRETO en el compose.yml — solo MI_SECRETO_FILE. Es el fallo más común al mezclar los dos escenarios: copiar el patrón de una imagen oficial (que lee _FILE por ti) y esperar que tu propio código haga lo mismo sin haberlo programado.

Buenas prácticas para gestionar Docker Secrets de forma segura

✅ Haz esto

  • chmod 600 en los archivos de secrets — solo el propietario puede leerlos
  • secrets/ siempre en .gitignore, sin excepciones
  • Un secret por credencial, no un .env gigante montado como secret
  • Documenta qué contiene cada secret en secrets/README.md sin valores reales

❌ Evita esto

  • Meter el valor real en ARG o ENV del Dockerfile — puede quedar expuesto en las capas o metadatos de la imagen.
  • Confiar en que rotar el archivo rota el secret automáticamente — necesitas reiniciar el contenedor
  • Commitear compose.override.yml con secrets de desarrollo "solo para probar"
  • Loguear el valor del secret para debug en producción y olvidarte de quitar esa línea

Docker Secrets en producción: cuándo es suficiente y cuándo no

Depende del tamaño de tu infraestructura. Para un VPS self-hosted con docker compose, esto ya es un salto de seguridad real frente a variables de entorno sueltas. Para un VPS self-hosted con Docker Compose, utilizar secrets basados en archivos puede reducir la exposición de credenciales frente a almacenarlas directamente como variables de entorno.

Si gestionas varios entornos, necesitas auditoría de accesos o rotación automática, el siguiente paso lógico es Docker Swarm secrets, o herramientas dedicadas como HashiCorp Vault, SOPS (cifrando los archivos de secrets con age o GPG antes de subirlos a git), o el gestor de secrets de tu proveedor cloud si en algún momento migras fuera del VPS.

Y ya que estás hardening el stack, no te quedes solo en los secrets — te recomiendo pasar por la guía de cómo escanear vulnerabilidades Docker con Trivy para revisar también qué CVEs arrastran tus imágenes base.

Preguntas frecuentes sobre Docker Secrets

¿Docker Secrets funciona sin Docker Swarm?

Sí, para secrets basados en archivo (file:). No necesitas Swarm — funciona con docker compose up normal. Lo que sí requiere Swarm es external: true junto con docker stack deploy.

¿Los secrets de Docker Compose están cifrados?

En modo Compose standalone, no en reposo — es un archivo normal en disco, solo que no se expone en docker inspect ni en las capas de la imagen. El cifrado en reposo real es una característica de Docker Swarm.

¿Cómo cambio un secret en Docker Compose?

En Docker Compose, cambiar el archivo que contiene el secret no actualiza automáticamente el contenido que ya está disponible dentro del contenedor. Para que el servicio utilice el nuevo valor, normalmente tendrás que recrear o reiniciar el servicio. Si necesitas una rotación coordinada entre varios servicios o sin interrupción del servicio, el procedimiento depende de la aplicación y de la plataforma utilizada. Docker Swarm dispone de mecanismos específicos para gestionar y reemplazar secrets.

¿Puedo usar secrets durante el build, no solo en runtime?

Sí, con BuildKit: docker build --secret id=mi_secret,src=./secret.txt monta el archivo temporalmente durante una instrucción RUN y lo elimina al terminar, sin dejarlo en ninguna capa.

¿Qué pasa si la imagen no soporta la convención _FILE?

Lees el archivo manualmente desde tu código de arranque — es la misma ruta /run/secrets/<nombre> para cualquier imagen, independientemente del lenguaje.

Read more