---
title: "Secretos en Docker Compose: Qué Funciona y Alternativas Seguras"
description: "Los secretos de Docker Compose explicados: qué hacen realmente, cuándo se necesitan los secretos de Swarm, cómo montar archivos de secretos de forma segura y alternativas seguras para producción."
date: 2024-09-06
categories: ["self-hosting"]
tags: ["docker","security","self-hosting"]
---

import Notice from "@components/widgets/Notice.astro";
import ListCheck from "@components/widgets/ListCheck.astro";
import Tabs from "@components/widgets/Tabs.astro";
import Tab from "@components/widgets/Tab.astro";
import Accordion from "@components/widgets/Accordion.astro";
import Button from "@components/widgets/Button.astro";

Los secretos de Docker Compose son uno de los rincones más incomprendidos del ecosistema Docker: el Compose normal solo monta archivos de secretos, mientras que los secretos reales —cifrados y con alcance por servicio— son una función exclusiva de Swarm. Esta guía muestra cómo inyectar contraseñas de bases de datos, claves de API y claves privadas sin filtrarlas en Git, capas de imagen, logs o variables de entorno.

Dos cosas importan desde el principio:

- **Los "secretos de Docker" con cifrado son una función de Swarm.** El Docker Compose normal no cifra los secretos en reposo; monta archivos. El cifrado real y el acceso con alcance por servicio solo ocurren en **Docker Swarm**.
- **Docker Compose aún puede montar datos secretos como archivos.** Eso es lo que hace la clave `secrets:` en Compose normal. La seguridad depende del filesystem de tu host, tus backups y tu disciplina.

Cubriré ambos caminos: los secretos reales de Swarm y cómo referenciarlos desde Compose (Opción A), luego los archivos de secretos de Compose para entornos locales y autoalojados, además de alternativas para producción (Opción B). Si solo quieres que `docker compose up` funcione con un archivo de contraseña, salta a la Opción B.

## Requisitos previos

<ListCheck>
<ul>
<li>Docker Engine 27+ con Compose v2 en cualquier host Linux. Confírmalo con <code>docker version</code> y <code>docker compose version</code>.</li>
<li>Root o sudo en ese host.</li>
<li>Una imagen de prueba a la que conectar los secretos. Postgres funciona de fábrica porque soporta <code>POSTGRES_PASSWORD_FILE</code>.</li>
<li><code>git</code>, para el paso del <code>.gitignore</code>.</li>
<li>Solo para la Opción A: una sola máquina es suficiente. Un Swarm de un nodo sirve para aprender esto.</li>
</ul>
</ListCheck>

Puedes seguir la guía en un portátil, o en [un VPS pequeño de Hetzner Cloud (~4 €/mes)](https://go.bitdoze.com/hetzner) si quieres una máquina desechable que puedas romper y reconstruir.

_Los precios de los VPS subieron en general en 2026 — si estás reconsiderando una máquina alquilada, mira [qué cambió y cuándo gana un mini PC](/es/subidas-precios-vps/)._

<Notice type="info" title="Versiones">La sintaxis aquí apunta a Docker Engine 27+ con Compose v2 (el actual en Ubuntu 24.04). La sintaxis de <code>secrets:</code> es estable desde Compose v2, pero verifica los flags contra docs.docker.com si tu Engine es más antiguo.</Notice>

## Secretos de Docker vs secretos de Docker Compose: cuál es la diferencia

### ¿Qué son los secretos de Docker (secretos de Swarm)?

**Los secretos de Docker** son una función de Swarm para gestionar datos sensibles: contraseñas, claves de API, claves privadas TLS, etc. Cuando ejecutas servicios en Swarm, los secretos están:

- **Cifrados en reposo** en el log Raft de Swarm en los managers
- **Cifrados en tránsito** entre los nodos de Swarm
- **Expuestos a los contenedores como archivos** en `/run/secrets/<name>`
- **Con alcance por servicio**: solo los servicios que los solicitan explícitamente
- **Rotables**: crea una versión nueva, actualiza el servicio, elimina la antigua

Este es el modelo de seguridad al que se refiere la gente cuando dice «secretos de Docker de verdad».

### ¿Qué son los «secretos» en Docker Compose (sin Swarm)?

En Docker Compose normal, una entrada `secrets:` es un bind mount ordenado: el archivo aparece en `/run/secrets/<name>` dentro del contenedor con una ruta estandarizada. Nada se cifra. Nada se almacena dentro de Docker. Lo que sí obtienes:

1. **Los secretos quedan fuera de las capas de imagen** porque se inyectan en runtime.
2. **Configuración de app más limpia** porque las apps leen `/run/secrets/...` en lugar de valores hardcodeados.
3. **Archivos amigables con el control de versiones** porque el archivo Compose referencia *archivos* de secretos, y esos archivos quedan fuera de Git.

Es genuinamente útil. Solo recuerda que el valor solo es tan seguro como el host donde reside.

![Diagrama que compara los montajes de archivos de secretos de Docker Compose normal con los secretos cifrados de Docker Swarm](@assets/images/24/09/docker-compose-secrets-swarm-vs-compose.svg)

*El Compose normal hace un bind mount de un archivo y ahí se queda. Swarm añade un almacén cifrado, distribución cifrada y alcance por servicio.*

Selector rápido antes de entrar en detalles:

| Enfoque | Úsalo cuando |
|---|---|
| Secretos de Swarm | Corres Swarm y quieres cifrado en reposo, alcance por servicio y rotación real |
| Archivos de secretos de Compose | Desarrollo, homelab, autoalojamiento en un solo host; tú proteges el host |
| Gestor de secretos (Vault, cloud) | Múltiples hosts o plataformas, requisitos de cumplimiento, rotación automatizada |

## Opción A (nivel producción): secretos de Docker Swarm

Los secretos de Docker requieren el modo Swarm. Si no corres Swarm, salta a la Opción B.

### 1) Inicializar Swarm (una sola vez)

```sh
docker swarm init
```

Un solo nodo es suficiente.

### 2) Crear un secreto

Evita `echo "secret" > file` siempre que puedas. Queda en el historial del shell y arrastra un salto de línea final (más sobre esto en los modos de fallo). Lee desde stdin en su lugar:

```sh
printf '%s' 'mysupersecretpassword' | docker secret create my_db_password -
```

El comando imprime el ID del nuevo secreto. Esa es tu confirmación de que existe.

O desde un archivo:

```sh
docker secret create my_db_password db_password.txt
chmod 400 db_password.txt
```

Listar los secretos:

```sh
docker secret ls
```

**Comandos habituales**

| Comando                       | Descripción                        |
|------------------------------|------------------------------------|
| `docker secret create`       | Crea un nuevo secreto               |
| `docker secret ls`           | Lista todos los secretos            |
| `docker secret inspect`      | Muestra los metadatos del secreto (no el valor) |
| `docker secret rm <secret>`  | Elimina un secreto                  |

Docker no te permite leer el valor de un secreto a través del CLI. Es intencionado. Para el conjunto completo de comandos, vale la pena tener abierta en otra pestaña la [chuleta de comandos esenciales de Docker](https://www.bitdoze.com/docker-commands/).

### Usar secretos de Swarm desde un archivo Compose

Si despliegas tu stack en Swarm (vía `docker stack deploy`), referencia un secreto de Swarm **externo**:

```yaml
services:
  app:
    image: myapp:latest
    secrets:
      - my_db_password
    environment:
      DB_PASSWORD_FILE: /run/secrets/my_db_password

secrets:
  my_db_password:
    external: true
```

```sh
docker stack deploy -c compose.yml demo
```

**Verifica que el secreto realmente llegó**, de fuera hacia dentro:

```sh
docker service ls
# expect: demo_app  replicated  1/1

docker service inspect demo_app --format '{{json .Spec.TaskTemplate.ContainerSpec.Secrets}}'
# expect: [{"SecretName":"my_db_password","File":{...}}]

docker exec "$(docker ps -q --filter name=demo_app)" cat /run/secrets/my_db_password
# expect: mysupersecretpassword
```

Si `docker service inspect` imprime `null`, el servicio nunca recibió el secreto. Corrige el archivo Compose antes de depurar dentro del contenedor.

### Leer secretos en los contenedores

Los secretos se montan como archivos en `/run/secrets/<secret_name>`. Lee el archivo al arrancar en lugar de pasar el valor por una variable de entorno:

<Tabs>
<Tab name="Python">

```python
from pathlib import Path

db_password = Path("/run/secrets/my_db_password").read_text(encoding="utf-8").strip()
```

</Tab>
<Tab name="Node.js">

```js
const fs = require("node:fs");

const dbPassword = fs.readFileSync("/run/secrets/my_db_password", "utf8").trim();
```

</Tab>
<Tab name="Bash">

```sh
DB_PASSWORD="$(cat /run/secrets/my_db_password)"
```

</Tab>
</Tabs>

Los tres leen el archivo al arranque, lo que mantiene el valor fuera del entorno. Esa es la superficie que exponen `docker inspect`, los crash dumps y los procesos hijo.

**Dónde muerde la Opción A:**

- `docker stack deploy` falla con `secret my_db_password not found` si el secreto aún no existe. Los secretos externos deben crearse **antes** del despliegue; el archivo del stack no los crea por ti.
- Un salto de línea final de `echo` o de un editor pasa a formar parte del valor. Los fallos de autenticación con una contraseña «correcta» casi siempre son esto. Regenéralo con `printf '%s'`.
- Si tu imagen de contenedor baja privilegios a un usuario no-root, un archivo `600` propiedad de root le queda ilegible. Corrige el propietario o corre el contenedor con un UID coincidente; mira cómo [correr contenedores como usuario no-root](https://www.bitdoze.com/add-users-to-docker-container/).

### Revertir la Opción A

Quita un secreto de un servicio en ejecución y luego elimínalo:

```sh
docker service update --secret-rm my_db_password demo_app
docker secret rm my_db_password
```

La actualización del servicio dispara un reinicio progresivo de las tareas, así que hazlo en una ventana tranquila. Ten en cuenta que los secretos son inmutables: no hay edición. «Cambiar» un secreto siempre significa crear uno nuevo, apuntar el servicio a él y eliminar el antiguo, que es lo que cubre la sección de rotación más abajo.

## Opción B (amigable con Compose): archivos de secretos de Docker Compose

Este es el camino para la mayoría de lectores: desarrollo local y autoalojamiento en un solo host, ya sea un VPS o un [mini PC compacto como servidor casero](/go/gmktec-m5-ultra/). Si gestionas varios stacks de Compose a mano, [gestionar tus stacks de Compose con Dockge](/es/instalar-dockge/) encaja muy bien con este flujo de trabajo.

### Paso 1: crea un directorio de secretos (y mantenlo fuera de Git)

```sh
mkdir -p secrets
printf '%s' 'change-me-before-use' > secrets/db_password.txt
chmod 600 secrets/db_password.txt
```

`.gitignore`:

```
secrets/
*.env
```

### Paso 2: declara el secreto en compose.yml

```yaml
services:
  db:
    image: postgres:17
    environment:
      POSTGRES_PASSWORD_FILE: /run/secrets/db_password
    secrets:
      - db_password

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

No hace falta la clave `version:`; Compose v2 la ignora y muestra un aviso si está presente. `POSTGRES_PASSWORD_FILE` la lee el entrypoint de Postgres en la primera inicialización, así que crea el volumen de datos después de que el secreto exista.

### Paso 3: verificar

```sh
docker compose config --quiet && echo "compose file OK"
# resolves the file, catches YAML errors; silence is good

docker compose up -d

docker compose exec db ls -l /run/secrets/
# expect: -rw------- ... db_password  (bind mount keeps host permissions)

docker compose exec db env | grep change-me
# expect: no output, the value never entered the environment
```

Y en el host:

```sh
stat -c '%a %U:%G' secrets/db_password.txt
# expect: 600 root:root
```

Cómo se ve el fallo: que `docker compose config` falle con un error «secret not found» significa que escribiste `external: true` en lugar de `file:`. Un `/run/secrets/` vacío dentro del contenedor significa que ese servicio nunca declaró el secreto.

Los archivos de secretos no son solo para contraseñas. Las claves privadas TLS siguen el mismo patrón de montaje, y puedes verlo de principio a fin en la configuración de [certificados wildcard de Traefik con Let's Encrypt y Cloudflare](/es/traefik-certificado-wildcard/).

### Llevar esto a producción

El mismo archivo compose se despliega en un PaaS autoalojado con un flujo de despliegue real, gestión de env y proxy encima. Si esa es la dirección que tomas, aquí está cómo [desplegar una app Docker Compose en Dokploy](/es/dokploy-app-docker-compose/).

### Secretos externos vs basados en archivo en Docker Compose

Verás dos patrones:

- **Basados en archivo (`file: ...`)**: «monta este archivo del host en el contenedor como `/run/secrets/<name>`». Es lo que la mayoría usa en Docker Compose normal.
- **Externos (`external: true`)**: «el secreto ya existe en la plataforma». Relevante para los **secretos de Swarm**, creados con `docker secret create` y compartidos entre servicios y stacks, gestionados con independencia del archivo Compose y rotables sin tocar el archivo.

| | `file:` (Compose normal) | `external: true` (Swarm) |
|---|---|---|
| Dónde vive el valor | Un archivo en el host | El almacén cifrado de Swarm |
| Cifrado en reposo | No | Sí |
| Funciona con `docker compose up` | Sí | No: «secret not found» |
| Funciona con `docker stack deploy` | Sí (montado tal cual) | Sí (el uso previsto) |
| Rotación | Sobrescribe el archivo, recrea los contenedores | Inmutable: añade v2, actualiza el servicio, elimina v1 |

Usa los basados en archivo para desarrollo, pruebas y autoalojamiento en un solo host. Usa secretos externos de Swarm en despliegues Swarm, sobre todo cuando un secreto lo comparten varios servicios.

#### Secretos de Docker vs variables de entorno en Compose

<Notice type="warning" title="No pongas valores secretos en variables de entorno">Algunas guías antiguas muestran secretos definidos directamente en el archivo Compose o traídos desde variables de entorno. Salta ese patrón. Los valores pasados por <code>environment:</code> son legibles en <code>docker inspect</code>, acaban en crash dumps y todos los procesos hijo que la app lance los heredan.</Notice>

Si aun así necesitas variables de entorno, mantén los roles separados:

- `.env` para configuración no sensible: puertos, tags de imagen, feature flags
- un gestor de secretos de verdad para valores sensibles en producción (Vault o un proveedor cloud)
- genera el archivo de secreto durante el despliegue y móntalo (nunca lo commitees)

Para la mecánica completa de la inyección, mira [cómo funcionan ARG y ENV en Docker y Docker Compose](/es/variables-entorno-docker/).

## Buenas prácticas para los secretos de Docker Compose

El objetivo central nunca cambia: mantén los secretos fuera de las imágenes, de Git y de los logs, y minimiza el radio de impacto cuando algo se filtra.

### Convenciones de nombres

- Nombres descriptivos: `prod_db_password` en lugar de `secret1`.
- Prefijos consistentes: `prod_` para producción, `dev_` para desarrollo.
- Nada de información sensible en los propios nombres.

### Control de versiones

- Nunca commitees valores secretos.
- Añade el directorio de secretos a `.gitignore` (`secrets/`).
- Mantén un directorio `secrets.example/` con archivos de marcador de posición, o documenta los nombres de archivo requeridos en un README.
- Trata los archivos `.env` como secretos si contienen contraseñas o tokens.

### Rotar secretos

En Swarm, la rotación es crear-nuevo, cambiar, eliminar-viejo:

```sh
printf '%s' 'new-password' | docker secret create my_db_password_v2 -

docker service update \
  --secret-rm my_db_password \
  --secret-add source=my_db_password_v2,target=my_db_password \
  demo_app

# after the rollout verifies:
docker secret rm my_db_password
```

El truco de `source=...,target=...` mantiene estable la ruta dentro del contenedor para que la config de la app no cambie.

Si el despliegue falla, revierte mientras el secreto antiguo aún existe:

```sh
docker service update \
  --secret-rm my_db_password_v2 \
  --secret-add my_db_password \
  demo_app
```

Por eso eliminas el secreto antiguo al final, nunca primero.

En Compose normal, la rotación es: sobrescribe el archivo y luego `docker compose up -d --force-recreate db`. Compose monta el archivo una vez al crear el contenedor y no lo vigila, así que sin recrearlo el contenedor sigue sirviendo el valor antiguo.

Y rota la credencial en el origen (el rol de base de datos, la clave de API del cloud), no solo dentro de Docker. Rotar el montaje pero no la credencial no rota nada que importe.

### Blindar el host

Con Compose normal, el host es el modelo de seguridad:

```sh
sudo chown root:root secrets
sudo chmod 700 secrets
sudo chown root:root secrets/*
sudo chmod 600 secrets/*
```

Si un contenedor corre con un UID no-root, haz `chown` del archivo concreto a ese UID o `chgrp` a un grupo al que pertenezca ese UID.

Los backups necesitan el mismo tratamiento: tu directorio de secretos acabará dentro de los backups del host, así que cifra los backups (restic con una clave age, o almacenamiento compatible con S3 con cifrado en servidor y credenciales restringidas) en lugar de enviar archivos de secretos en texto plano junto a tus dumps de base de datos.

Para un servidor expuesto a internet, empieza con [cómo proteger un servidor Docker expuesto a internet](/es/informe-seguridad-bsi-docker-ufw/), y añade bloqueo automatizado con [CrowdSec para proteger tu VPS](/es/crowdsec-servidor-seguro/).

<Notice type="warning" title="Un secreto montado no protege un puerto publicado">Un servicio con secretos publicado en <code>0.0.0.0</code> está expuesto por muy bien que montes el secreto. Docker publica puertos a través de iptables y puede saltarse las reglas de UFW; lee [por qué Docker se salta las reglas del firewall UFW](/es/docker-evita-firewall/) antes de confiar en un firewall que no has verificado.</Notice>

### Hábitos adicionales que importan

- Declara los secretos por servicio. Solo los servicios que necesitan un secreto deberían verlo.
- Prefiere el consumo por archivo: usa variables `*_FILE` en lugar de meter valores en el entorno.
- No imprimas secretos. Evita loguear objetos de configuración que incluyan rutas o valores de secretos.
- Para producción seria entre varios hosts, pasa a un gestor de secretos de verdad, cubierto en las alternativas de abajo.

## Solución de problemas con los secretos de Docker Compose

| Síntoma / error | Causa | Solución |
|---|---|---|
| `secret my_db_password not found` en `docker compose up` | `external: true` solo se resuelve bajo Swarm / `docker stack deploy` | En Compose normal usa `file:` con una ruta, o crea primero el secreto en Swarm |
| La autenticación de la app falla aunque la contraseña parece correcta | Un salto de línea final de `echo` o de un editor pasó a ser parte del valor | Regenéralo con `printf '%s'`; comprueba el último byte con `xxd secrets/db_password.txt` (no debe terminar en `0a`). Postgres recorta los saltos de línea finales; la mayoría de las demás imágenes no |
| `permission denied` al leer `/run/secrets/<name>` | El contenedor corre como no-root y el bind mount mantiene propiedad de root | Haz `chown` del archivo del host al UID del contenedor, o define `user:` en compose (mira los modos de fallo en la Opción A) |
| Valor secreto horneado en la imagen | Intentaste hacerle `COPY` o pasarlo como `ARG` de build | Los secretos son solo de runtime por diseño; usa los montajes `--secret` de BuildKit para los builds, que no persisten en las capas |
| Valor visible en la salida de `docker inspect` o `docker compose config` | Se pasó vía `environment:` | Muévelo a `secrets:` más una variable `*_FILE` |
| La app no soporta `*_FILE` | La app solo lee variables de entorno planas | Un pequeño wrapper en el entrypoint: `export DB_PASSWORD="$(cat /run/secrets/db_password)"` y luego `exec "$@"`. Compromiso: el valor vuelve al entorno de ese árbol de procesos |
| Editaste el archivo del host y el contenedor sigue mostrando el valor antiguo | Compose monta el archivo una vez al crear el contenedor; los editores reemplazan el archivo (nuevo inodo), rompiendo el bind mount | `docker compose up -d --force-recreate <service>` |

## Ejemplos de secretos de Docker Compose

Cuatro casos habituales. La pestaña de Postgres es el camino completo sin tropiezos; las demás son fragmentos listos para insertar. Todas usan `file:` porque es lo que soporta el Compose normal. En Swarm, cambia `file:` por `external: true` y crea primero el secreto con el CLI.

<Tabs>
<Tab name="Postgres">

```yaml
services:
  db:
    image: postgres:17
    environment:
      POSTGRES_PASSWORD_FILE: /run/secrets/db_password
    secrets:
      - db_password
    volumes:
      - pgdata:/var/lib/postgresql/data

  app:
    image: myapp:latest
    secrets:
      - db_password
    environment:
      DB_PASSWORD_FILE: /run/secrets/db_password

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

volumes:
  pgdata:
```

Generar y verificar:

```sh
printf '%s' "$(openssl rand -base64 24)" > secrets/db_password.txt
chmod 600 secrets/db_password.txt
docker compose up -d
docker compose logs db | grep -m1 "database system is ready"
# expect: ... database system is ready to accept connections

docker compose exec app ls -l /run/secrets/
# expect: db_password listed with restrictive permissions
```

</Tab>
<Tab name="Clave API">

```yaml
services:
  worker:
    image: myapp:latest
    secrets:
      - api_key
    environment:
      API_KEY_FILE: /run/secrets/api_key

secrets:
  api_key:
    file: ./secrets/api_key.txt
```

Si la app no soporta la convención `*_FILE`, usa el wrapper de entrypoint de la tabla de problemas.

</Tab>
<Tab name="Certificados TLS">

```yaml
services:
  web:
    image: nginx:1.27
    secrets:
      - source: site_certificate
        target: site.crt
      - source: site_key
        target: site.key
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro

secrets:
  site_certificate:
    file: ./certs/site.crt
  site_key:
    file: ./certs/site.key
```

Con `target:`, los archivos aparecen en `/run/secrets/site.crt` y `/run/secrets/site.key`. Apunta ahí las directivas `ssl_certificate` de tu `nginx.conf`. El proceso maestro de Nginx corre como root, así que una clave `600` es legible. Es el mismo patrón que la guía de certificados wildcard de Traefik (enlazada en la Opción B) recorre de principio a fin.

</Tab>
<Tab name="Claves JWT">

```yaml
services:
  auth:
    image: myapp:latest
    secrets:
      - jwt_private_key
      - jwt_public_key
    environment:
      JWT_PRIVATE_KEY_FILE: /run/secrets/jwt_private_key
      JWT_PUBLIC_KEY_FILE: /run/secrets/jwt_public_key

secrets:
  jwt_private_key:
    file: ./secrets/jwt_private_key.pem
  jwt_public_key:
    file: ./secrets/jwt_public_key.pem
```

Aplica `chmod 600` a la clave privada. La clave pública puede quedarse en `644`; los servicios a menudo necesitan verificar tokens sin poseer la mitad privada.

</Tab>
</Tabs>

## Limitaciones de los secretos de Docker Compose y alternativas seguras

### El requisito del modo Swarm

Los secretos de Docker de verdad requieren **Swarm**. El Compose normal solo te da montajes de archivos: sin almacén cifrado, sin alcance por servicio impuesto por un orquestador. Para Kubernetes, usa Kubernetes Secrets (idealmente con cifrado en reposo activado e integración con un gestor de secretos externo).

Con eso en mente, aquí están las alternativas, con el coste operativo honesto de cada una:

1. **Archivos de secretos montados vía Compose**
   - Pros: simple, mantiene los secretos fuera de las capas de imagen, funciona dondequiera que corra Compose
   - Contras: la seguridad depende del filesystem del host, los backups y la disciplina operativa
   - Es el default sensato para un solo host.

2. **Variables de entorno (usar con cautela)**
   - Pros: simples, soportadas universalmente
   - Contras: fáciles de filtrar vía `docker inspect`, crash dumps, logs o procesos hijo
   - Evítalas para valores muy sensibles cuando existe una opción basada en archivos.

3. **Gestores de secretos en la nube**
   - AWS Secrets Manager, SSM Parameter Store, Google Secret Manager, Azure Key Vault
   - Pros: acceso basado en IAM, logs de auditoría, rotación gestionada
   - Contras: acoplamiento a la plataforma; el precio es por secreto más por consulta, aunque los tiers gratuitos suelen cubrir los secretos de un homelab

4. **HashiCorp Vault**
   - Pros: agnóstico de plataforma, secretos dinámicos, auditabilidad
   - Contras: un servicio con estado más que respaldar, actualizar y desellar. Exagerado por debajo de unos cinco servicios o sin un requisito real de cumplimiento

5. **Kubernetes Secrets (más operadores de secretos externos)**
   - Pros: de primera clase en el ecosistema k8s, se integran con almacenes de secretos
   - Contras: requiere k8s; los Secrets básicos se almacenan en etcd sin cifrar salvo que actives el cifrado en reposo
   - Combínalos con el External Secrets Operator para sincronizar desde un gestor de secretos en la nube.

6. **Docker Config**
   - La misma maquinaria de Swarm para configuración *no sensible*. Úsalo junto a los secretos para mantener config y credenciales en formas consistentes

Mi ranking honesto para un autoalojador en solitario: primero archivos de secretos de Compose, segundo un PaaS autoalojado, Vault o un gestor cloud solo cuando un requisito real lo exige. Si gestionar archivos de secretos a mano te suena a tarea pesada, el punto medio es un PaaS autoalojado: [comparativa de plataformas PaaS autoalojadas](/es/coolify-vs-dokploy-vs-kamal-2/) cubre las opciones, y guardan env y secretos por app por ti manteniendo tus archivos compose.

## FAQ

<Accordion label="¿Los secretos de Docker Compose están cifrados?" group="faq" expanded="true">

No. En Compose normal, `secrets:` es un montaje de archivo y no añade cifrado. El cifrado en reposo en el log Raft y la distribución cifrada entre nodos son funciones exclusivas de Docker Swarm. Con Compose normal, tu modelo de seguridad es el filesystem del host: permisos, cifrado de disco y backups.

</Accordion>

<Accordion label="¿Funcionan los secretos de Compose sin Swarm?" group="faq" expanded="false">

Sí, como montajes de archivos planos. Declara el secreto con `file:` en tu archivo compose y la app lo lee en `/run/secrets/<name>`. La variante `external: true` es la que requiere Swarm.

</Accordion>

<Accordion label="¿Son los secretos de Docker mejores que las variables de entorno?" group="faq" expanded="false">

En general sí para valores sensibles. Leer un archivo al arranque mantiene el valor fuera de la salida de `docker inspect`, fuera de los crash dumps y fuera del entorno que heredan los procesos hijo. No es perfecto (un dump de memoria aún puede filtrarlo), pero cierra las rutas de fuga más comunes. Queda algo de superficie de fuga por diseño cuando una app solo acepta env vars; usa un wrapper de entrypoint como último recurso.

</Accordion>

<Accordion label="¿Dónde se almacenan los secretos de Compose en el host?" group="faq" expanded="false">

Exactamente donde apunta tu `file:`, por ejemplo `./secrets/db_password.txt`. Compose hace bind-mount del archivo dentro del contenedor en el momento de la creación y no copia nada en la imagen ni en el almacenamiento propio de Docker. Borra el archivo y el secreto desaparece para los contenedores futuros. En Swarm, los valores viven en el log Raft cifrado de los nodos manager.

</Accordion>

<Accordion label="¿Cómo roto un secreto sin downtime?" group="faq" expanded="false">

En Swarm: crea un secreto nuevo versionado, `docker service update` para apuntar el servicio a él, y elimina el secreto antiguo después de verificar el despliegue. Los secretos son inmutables, así que no hay edición en el lugar. En Compose normal: sobrescribe el archivo y luego `docker compose up -d --force-recreate`. Los comandos de rotación y el camino de reversión están en la sección de buenas prácticas de arriba.

</Accordion>

<Accordion label="¿Puedo usar secretos durante docker build?" group="faq" expanded="false">

No con `secrets:`. Los secretos de Compose son construcciones de runtime; cualquier cosa que hornees en una capa se queda en la imagen para siempre, incluso en capas que creías haber eliminado. Para los builds, usa los montajes de secretos de BuildKit: `docker build --secret id=mytoken,src=./secrets/token.txt` con `RUN --mount=type=secret,id=mytoken` en el Dockerfile. Se exponen solo en tiempo de build y no persisten en las capas.

</Accordion>

## Conclusión: ¿qué enfoque de secretos de Docker deberías usar?

La regla de decisión que yo aplicaría:

- ¿Corres Swarm multi-nodo, o quieres cifrado y alcance gestionados por Docker? Usa secretos de Swarm (Opción A).
- ¿Compose normal en un solo host? Usa archivos de secretos (Opción B), blinda el host y cifra los backups.
- ¿Requisitos de cumplimiento, múltiples plataformas o Kubernetes? Un gestor de secretos de verdad, o k8s Secrets con cifrado en reposo.

<ListCheck>
<ul>
<li>El cifrado es solo de Swarm. Los secretos de Compose normal son montajes de archivos, nada más.</li>
<li>Los secretos y las variables de entorno son herramientas distintas. Prefiere lecturas <code>*_FILE</code> y mantén los valores fuera de <code>docker inspect</code>.</li>
<li>Verifica la entrega con <code>cat /run/secrets/...</code> dentro del contenedor en lugar de asumir que el montaje funcionó.</li>
<li>En Compose normal, el host es el perímetro: permisos de archivos, backups cifrados, firewall verificado.</li>
</ul>
</ListCheck>

Elijas el camino que elijas, prefiere aplicaciones que lean desde `/run/secrets/...` y mantén los valores secretos fuera de las imágenes, de Git y de los logs. Si el siguiente paso en tu lista es el hardening del host, empieza aquí:

<Button text="Protege tu servidor Docker ahora" link="/es/informe-seguridad-bsi-docker-ufw/" variant="solid" color="blue" size="md" />