Pasar contraseñas a un contenedor con variables de entorno es cómodo, pero esas variables aparecen en docker inspect, se heredan en los procesos hijos y acaban a menudo en logs o volcados de error. Los secretos de Docker entregan cada credencial como un archivo dentro del contenedor, en /run/secrets/, que solo ven los servicios autorizados. En este tutorial usarás secretos con Docker Compose en un único servidor, después con Docker Swarm (donde se cifran y distribuyen entre nodos), aprenderás a rotarlos y a pasar credenciales durante la construcción de una imagen sin que queden en ella. Todo en Ubuntu 24.04.
Requisitos previos
Para seguir esta guía necesitas:
- Un servidor con Ubuntu 24.04 LTS, por ejemplo un VPS de CubePath, con un usuario no root con privilegios
sudo. - Docker Engine y el plugin de Docker Compose instalados desde el repositorio oficial de Docker.
- Tu usuario en el grupo
docker, o anteponersudoa los comandosdocker.
Paso 1: Ver el problema de las variables de entorno
Arranca un contenedor de PostgreSQL pasando la contraseña como variable de entorno:
docker run -d --name pg-env -e POSTGRES_PASSWORD=supersecreto postgres:16
Cualquiera con acceso al socket de Docker puede leerla sin entrar en el contenedor:
docker inspect pg-env --format '{{json .Config.Env}}'
["POSTGRES_PASSWORD=supersecreto","PATH=/usr/local/sbin:...","PG_MAJOR=16",...]
Elimina el contenedor de prueba:
docker rm -f pg-env
Con secretos, la variable contiene solo la ruta a un archivo y el valor no aparece en la configuración del contenedor. Las imágenes oficiales de PostgreSQL, MySQL, MariaDB y otras admiten variables con sufijo _FILE (por ejemplo POSTGRES_PASSWORD_FILE) precisamente para leer la credencial desde un archivo.
Paso 2: Usar secretos con Docker Compose
Docker Compose admite secretos sin necesidad de Swarm. En este modo el secreto es un archivo del servidor que Compose monta en /run/secrets/<nombre> dentro del contenedor. No se cifra, pero sale de las variables de entorno y del archivo compose.yaml, que así puedes versionar sin credenciales.
Crea el directorio del proyecto y un subdirectorio para los secretos accesible solo por tu usuario:
mkdir -p ~/secrets-demo/secrets && cd ~/secrets-demo
chmod 700 secrets
Genera una contraseña aleatoria y guárdala sin salto de línea final:
openssl rand -base64 32 | tr -d '\n' > secrets/db_password.txt
chmod 644 secrets/db_password.txt
El archivo tiene permisos 644 porque se monta tal cual en el contenedor y el proceso de PostgreSQL, que se ejecuta con el usuario postgres, tiene que poder leerlo. Lo que lo protege en el servidor es el directorio secrets/ con permisos 700.
Evita que el directorio acabe en Git:
echo "secrets/" >> .gitignore
Crea el archivo de Compose:
nano compose.yaml
services:
db:
image: postgres:16
environment:
POSTGRES_USER: app
POSTGRES_DB: app
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
secrets:
- db_password
volumes:
- db_data:/var/lib/postgresql/data
secrets:
db_password:
file: ./secrets/db_password.txt
volumes:
db_data:
La sección secrets de nivel superior declara de dónde sale cada secreto y la clave secrets del servicio indica qué servicios pueden usarlo. Un servicio que no lo declare no verá el archivo.
Levanta el servicio:
docker compose up -d
Comprueba que el secreto está montado y que la contraseña ya no aparece en el entorno:
docker compose exec db ls -l /run/secrets/
docker compose exec db env | grep POSTGRES_PASSWORD
total 4
-rw-r--r-- 1 1000 1000 44 Sep 25 10:20 db_password
POSTGRES_PASSWORD_FILE=/run/secrets/db_password
Verifica que la contraseña funciona conectándote por TCP con el valor del archivo:
docker compose exec -e PGPASSWORD="$(cat secrets/db_password.txt)" db psql -h 127.0.0.1 -U app -d app -c 'SELECT 1;'
?column?
----------
1
(1 row)
Detén la pila antes de pasar a Swarm:
docker compose down
Paso 3: Inicializar Docker Swarm y crear un secreto
En Docker Swarm los secretos se guardan cifrados en el registro Raft de los nodos manager, viajan por TLS solo a los nodos que ejecutan un servicio autorizado y se montan en memoria (tmpfs) dentro del contenedor. Nunca se escriben en claro en el disco de los workers.
Inicializa un Swarm de un solo nodo. Si el servidor tiene varias IP, indica la privada con --advertise-addr:
docker swarm init --advertise-addr your_server_ip
Swarm initialized: current node (k2x9...) is now a manager.
Crea el secreto leyendo el valor desde la entrada estándar (el - final). Así no queda en el historial de la shell ni en ningún archivo:
openssl rand -base64 32 | tr -d '\n' | docker secret create db_password -
Lista los secretos. Docker muestra sus metadatos, pero nunca su contenido:
docker secret ls
ID NAME DRIVER CREATED UPDATED
p3j8t0x4wz1m6k2c9v7b5n0qa db_password 10 seconds ago 10 seconds ago
docker secret inspect db_password tampoco muestra el valor. Una vez creado, un secreto no se puede leer desde la CLI ni modificar: solo se puede usar o eliminar.
Paso 4: Desplegar una pila que usa el secreto
Con Swarm despliegas pilas con docker stack deploy, usando el mismo formato de Compose. Crea un archivo nuevo para la pila:
nano stack.yaml
services:
db:
image: postgres:16
environment:
POSTGRES_USER: app
POSTGRES_DB: app
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
secrets:
- db_password
volumes:
- db_data:/var/lib/postgresql/data
deploy:
replicas: 1
secrets:
db_password:
external: true
volumes:
db_data:
external: true indica que el secreto ya existe en el Swarm y no debe crearse a partir de un archivo.
Despliega la pila:
docker stack deploy -c stack.yaml app
Comprueba que el servicio está en marcha:
docker service ls
ID NAME MODE REPLICAS IMAGE PORTS
r8f2k1m0c5qz app_db replicated 1/1 postgres:16
Comprueba el montaje dentro del contenedor de la tarea:
docker exec "$(docker ps -q -f name=app_db)" ls -l /run/secrets/
total 4
-r--r--r-- 1 root root 44 Sep 25 10:31 db_password
Por defecto Swarm monta los secretos con propietario root y modo 0444. Si tu aplicación necesita otro propietario, un nombre de archivo distinto o permisos más restrictivos, usa la sintaxis larga en el servicio:
secrets:
- source: db_password
target: db_password
uid: "999"
gid: "999"
mode: 0400
En la imagen oficial de PostgreSQL el usuario postgres tiene UID y GID 999.
Paso 5: Rotar un secreto
Como un secreto de Swarm es inmutable, rotarlo consiste en crear uno nuevo y cambiar el servicio para que lo use. El truco es montar el secreto nuevo con el mismo nombre de archivo (target) que el antiguo, para no tener que tocar la configuración de la aplicación.
Crea la nueva versión:
openssl rand -base64 32 | tr -d '\n' | docker secret create db_password_v2 -
Actualiza el servicio: quita el secreto antiguo y añade el nuevo en la misma ruta. Swarm sustituye las tareas una a una:
docker service update \
--secret-rm db_password \
--secret-add source=db_password_v2,target=db_password \
app_db
app_db
overall progress: 1 out of 1 tasks
1/1: running [==================================================>]
verify: Service app_db converged
Cuando ningún servicio usa ya el secreto antiguo, elimínalo:
docker secret rm db_password
Importanterotar el secreto solo cambia el archivo que ve el contenedor. PostgreSQL lee
POSTGRES_PASSWORD_FILEúnicamente al inicializar un volumen vacío, así que en una base de datos existente debes cambiar antes la contraseña del usuario (ALTER USER app WITH PASSWORD '...') y después rotar el secreto en los servicios que se conectan a ella. Lo mismo ocurre con cualquier credencial que viva también en otro sistema.
Si después actualizas stack.yaml, cambia la declaración a db_password_v2 (con external: true) y la referencia del servicio a la sintaxis larga con target: db_password, o el siguiente docker stack deploy intentará volver al secreto borrado.
Paso 6: Usar secretos durante el build
Los secretos de Compose y Swarm existen solo en tiempo de ejecución. Si necesitas una credencial para construir la imagen (un token para descargar paquetes privados, por ejemplo), no la pases con ARG ni la copies con COPY: quedaría grabada en el historial o en una capa de la imagen. BuildKit, el motor de build por defecto de Docker, permite montar un secreto solo durante una instrucción RUN.
Crea un directorio de prueba y un token de ejemplo:
mkdir -p ~/build-secret-demo && cd ~/build-secret-demo
printf 'token-de-ejemplo' > api_token.txt
Crea el Dockerfile:
nano Dockerfile
FROM alpine:3.20
RUN --mount=type=secret,id=api_token \
echo "El token tiene $(wc -c < /run/secrets/api_token) bytes" > /build-info.txt
En un caso real, esa instrucción RUN usaría el token para autenticarse, por ejemplo leyéndolo en la variable de un gestor de paquetes. Construye la imagen pasando el secreto:
docker build --secret id=api_token,src=api_token.txt -t build-secret-demo .
Comprueba que el build pudo leerlo y que el token no ha quedado en la imagen:
docker run --rm build-secret-demo sh -c 'cat /build-info.txt; ls /run/secrets 2>&1'
El token tiene 16 bytes
ls: /run/secrets: No such file or directory
El secreto solo estuvo montado mientras se ejecutaba esa instrucción RUN.
Paso 7: Proteger las claves del Swarm con autolock
Las claves que cifran el registro Raft se guardan en el disco de cada manager. Si alguien obtiene una copia del disco, puede descifrar los secretos. Con autolock, un manager que se reinicia necesita una clave de desbloqueo antes de volver a funcionar:
docker swarm update --autolock=true
Swarm updated.
To unlock a swarm manager after it restarts, run the `docker swarm unlock`
command and provide the following key:
SWMKEY-1-...
Guarda esa clave en tu gestor de contraseñas. Tras reiniciar Docker en un manager, desbloquéalo con:
docker swarm unlock
Valora si te compensa: autolock mejora la protección de los secretos a cambio de un paso manual en cada reinicio de un manager.
Solución de problemas
secret not found: db_password al desplegar la pila. El secreto está marcado como external: true pero no existe en el Swarm. Créalo con docker secret create o revisa el nombre con docker secret ls.
secret 'db_password' is in use by the following service al borrar. Algún servicio lo sigue usando. Quítalo antes con docker service update --secret-rm.
PostgreSQL no arranca con Permission denied al leer el secreto en Compose. El archivo del servidor no es legible para el UID del contenedor. Dale permisos 644 y protege el directorio que lo contiene.
this node is not a swarm manager. Los comandos docker secret solo funcionan en un nodo manager de Swarm. Con Compose sin Swarm usa la clave file: como en el paso 2.
Conclusión
Has sacado las credenciales de las variables de entorno usando secretos de Compose en un único servidor y secretos cifrados de Swarm, has rotado un secreto sin reconstruir imágenes y has pasado un token al build sin dejarlo en la imagen. Como siguientes pasos, puedes añadir nodos worker al Swarm, centralizar las credenciales en un gestor externo como HashiCorp Vault o preparar un procedimiento de rotación periódica para las contraseñas de tus bases de datos.
