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 anteponer sudo a los comandos docker.

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

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.