DevToolBoxGRATIS
Blog

Docker Compose Secrets y variables de entorno: La forma correcta

10 min de lecturapor DevToolBox

Las variables de entorno son el mecanismo principal para configurar contenedores en Docker Compose. Pero hay 4 formas diferentes de pasarlas, cada una con comportamientos, implicaciones de seguridad y trampas distintas. Esta guia cubre todos los enfoques.

4 formas de pasar variables de entorno en Docker Compose

Docker Compose soporta cuatro metodos distintos para inyectar variables de entorno en contenedores.

1. En linea con la directiva environment

Define pares clave-valor directamente en docker-compose.yml. Ideal para configuracion no sensible y especifica del servicio.

services:
  api:
    image: node:20-alpine
    environment:
      NODE_ENV: production
      DB_HOST: postgres
      DB_PORT: "5432"
      LOG_LEVEL: info

  # List syntax (also valid)
  worker:
    image: node:20-alpine
    environment:
      - NODE_ENV=production
      - QUEUE_NAME=tasks
      - CONCURRENCY=4

2. Archivo externo con env_file

Carga variables desde archivos externos. Ideal para gestionar grupos de variables relacionadas.

services:
  api:
    image: node:20-alpine
    env_file:
      - .env              # Shared variables
      - .env.api          # Service-specific variables
      - .env.local        # Local overrides (gitignored)

  db:
    image: postgres:16
    env_file:
      - .env
      - .env.db

3. Paso de variables del shell

Pasa variables del shell del host al contenedor. Ideal para pipelines CI/CD.

# In your shell:
export API_KEY=sk-abc123
export DEBUG=true

# docker-compose.yml
services:
  api:
    image: node:20-alpine
    environment:
      - API_KEY          # Passes host $API_KEY into container
      - DEBUG            # Passes host $DEBUG into container
      - CI               # Passes host $CI (empty string if unset)

4. Sustitucion de variables en docker-compose.yml

Interpola variables de entorno del host directamente en los valores del archivo Compose.

# .env file
POSTGRES_VERSION=16
APP_PORT=3000
REPLICAS=3

# docker-compose.yml
services:
  db:
    image: postgres:${POSTGRES_VERSION}    # Resolved from .env or shell
  app:
    ports:
      - "${APP_PORT}:3000"                 # Resolved from .env or shell
    deploy:
      replicas: ${REPLICAS}               # Resolved from .env or shell

Tabla comparativa

MetodoIdeal paraEn VCS?Seguro?
environmentNon-sensitive defaultsYesNo
env_fileGrouped config, secretsNo (.gitignore)Partial
Shell passthroughCI/CD, dynamic valuesN/APartial
${} substitutionCompose file templatingYes (file), No (.env)No

Archivo .env: sintaxis, ubicacion e interpolacion

Docker Compose lee automaticamente un archivo .env en el mismo directorio que docker-compose.yml.

Reglas de sintaxis

# Comments start with #
# Blank lines are ignored

# Simple key=value (no spaces around =)
DB_HOST=localhost
DB_PORT=5432

# Values with spaces need quotes
APP_NAME="My Docker App"
GREETING='Hello World'

# Multi-line values (double quotes only)
PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEA...
-----END RSA PRIVATE KEY-----"

# Variable expansion (Compose v2.20+)
BASE_URL=https://api.example.com
HEALTH_URL=${BASE_URL}/health

# Export prefix is ignored (compatible with shell source)
export SECRET_KEY=mysecretkey

# Empty values
EMPTY_VAR=
ALSO_EMPTY=''

Ubicacion y prioridad del archivo

Compose busca .env en el directorio del proyecto. Puede sobrescribirse con --env-file:

# Default: reads .env from project directory
docker compose up

# Override with --env-file
docker compose --env-file .env.staging up

# Multiple --env-file flags (Compose v2.24+)
docker compose --env-file .env --env-file .env.local up

# Project directory structure:
my-project/
  docker-compose.yml    # Compose file
  .env                  # Auto-loaded by Compose
  .env.staging          # Loaded with --env-file
  .env.production       # Loaded with --env-file

Interpolacion en docker-compose.yml

Las variables de .env estan disponibles para sustitucion en docker-compose.yml, pero NO se inyectan automaticamente en los contenedores:

# .env
TAG=3.2.1
EXTERNAL_PORT=8080

# docker-compose.yml
services:
  web:
    image: myapp:${TAG}              # Uses TAG from .env -> myapp:3.2.1
    ports:
      - "${EXTERNAL_PORT}:80"        # Uses EXTERNAL_PORT from .env -> 8080:80
    environment:
      - TAG                           # WRONG! This passes host $TAG, not .env TAG
      - TAG=${TAG}                   # RIGHT! Explicitly interpolate .env value

# Key insight:
# .env -> docker-compose.yml interpolation   (automatic)
# .env -> container environment              (NOT automatic, use env_file)

env_file vs environment: diferencias y trampas

Estas dos directivas parecen similares pero se comportan diferente.

DirectivaComportamiento
environment:Inlined in docker-compose.yml. Visible in version control. Supports variable substitution. Overrides env_file.
env_file:Loaded from external file(s). Can be gitignored. No variable substitution inside the file. Lower priority than environment.
.env (auto)Only for Compose file interpolation (${} syntax). NOT injected into containers. Lowest priority.

Trampas criticas

  • 1. Precedencia: los valores de environment sobrescriben los de env_file para la misma clave.
  • 2. El archivo .env (cargado automaticamente) es solo para interpolacion del archivo Compose. NO inyecta variables en contenedores.
  • 3. Las rutas de env_file son relativas al archivo docker-compose.yml, no al directorio de trabajo actual.
  • 4. Si falta un env_file, Compose fallara. Use required: false (Compose v2.24+) para hacerlo opcional.
# Precedence demonstration:

# .env.shared
DB_HOST=shared-db
DB_PORT=5432

# docker-compose.yml
services:
  api:
    env_file:
      - .env.shared              # DB_HOST=shared-db, DB_PORT=5432
    environment:
      DB_HOST: override-db       # Overrides env_file! DB_HOST=override-db
      # DB_PORT not listed here  # Keeps env_file value: DB_PORT=5432

# Result inside container:
# DB_HOST=override-db    (from environment, overrides env_file)
# DB_PORT=5432           (from env_file)

# Optional env_file (Compose v2.24+):
services:
  api:
    env_file:
      - path: .env.local
        required: false          # Won't fail if file doesn't exist

Docker Secrets: secretos basados en archivos para produccion

Los secretos Docker proporcionan una alternativa mas segura a las variables de entorno para datos sensibles.

Paso 1: Crear archivos de secretos

# Create secret files (don't commit these!)
echo "SuperSecretPassword123" > db_password.txt
echo "sk-prod-abc123xyz789" > api_key.txt

# Add to .gitignore
echo "*.txt" >> .gitignore
echo "secrets/" >> .gitignore

# Alternative: use a secrets directory
mkdir secrets
echo "SuperSecretPassword123" > secrets/db_password
echo "sk-prod-abc123xyz789" > secrets/api_key

Paso 2: Definir secretos en docker-compose.yml

# docker-compose.yml
services:
  db:
    image: postgres:16
    environment:
      POSTGRES_PASSWORD_FILE: /run/secrets/db_password  # Note: _FILE suffix
    secrets:
      - db_password                    # Grant access to this secret

  api:
    image: myapp:latest
    secrets:
      - db_password
      - api_key
    # Secrets available at:
    # /run/secrets/db_password
    # /run/secrets/api_key

# Top-level secrets definition
secrets:
  db_password:
    file: ./secrets/db_password        # From local file
  api_key:
    file: ./secrets/api_key            # From local file

  # Alternative: use environment variable as source
  # jwt_secret:
  #   environment: JWT_SECRET_VALUE    # From host env var (Compose v2.23+)

Paso 3: Leer secretos en la aplicacion

# Inside the container, secrets are plain text files:
$ docker compose exec api cat /run/secrets/db_password
SuperSecretPassword123

$ docker compose exec api cat /run/secrets/api_key
sk-prod-abc123xyz789

$ docker compose exec api ls -la /run/secrets/
total 8
-r--r--r-- 1 root root 24 Jan  1 00:00 db_password
-r--r--r-- 1 root root 22 Jan  1 00:00 api_key

Paso 4: Manejar secretos en el codigo

// Node.js - Read secret from file
const fs = require('fs');

function getSecret(name) {
  const secretPath = `/run/secrets/${name}`;
  try {
    return fs.readFileSync(secretPath, 'utf8').trim();
  } catch (err) {
    // Fallback to environment variable (for development)
    return process.env[name.toUpperCase()];
  }
}

const dbPassword = getSecret('db_password');
const apiKey = getSecret('api_key');
# Python - Read secret from file
import os

def get_secret(name: str) -> str:
    secret_path = f"/run/secrets/{name}"
    try:
        with open(secret_path) as f:
            return f.read().strip()
    except FileNotFoundError:
        # Fallback to environment variable
        return os.environ.get(name.upper(), "")

db_password = get_secret("db_password")
api_key = get_secret("api_key")

Por que Secrets en lugar de variables de entorno?

  • Las variables de entorno pueden filtrarse via docker inspect, /proc/environ y logs de error
  • Los secretos son basados en archivos y solo accesibles en /run/secrets/
  • Los secretos soportan control de acceso granular por servicio
  • Muchas bases de datos soportan nativamente el sufijo _FILE para archivos de secretos

Sustitucion de variables: sintaxis ${VAR:-default}

Docker Compose soporta sustitucion de variables estilo shell con valores por defecto.

SintaxisResultado
${VAR}Value of VAR. Error if unset.
${VAR:-default}Value of VAR if set, otherwise "default".
${VAR-default}Value of VAR if set (even if empty), otherwise "default".
${VAR:?error msg}Value of VAR if set, otherwise exit with "error msg".
${VAR?error msg}Value of VAR if set (even if empty), otherwise exit with "error msg".
${VAR:+replacement}"replacement" if VAR is set and non-empty, otherwise empty.
${VAR+replacement}"replacement" if VAR is set (even if empty), otherwise empty.

Ejemplo practico

# docker-compose.yml with variable substitution
services:
  web:
    image: nginx:${NGINX_VERSION:-1.25-alpine}     # Default: 1.25-alpine
    ports:
      - "${WEB_PORT:-80}:80"                        # Default: 80
      - "${SSL_PORT:-443}:443"                      # Default: 443
    volumes:
      - ${CONFIG_PATH:-./nginx.conf}:/etc/nginx/nginx.conf:ro

  api:
    image: ${REGISTRY:-docker.io}/myapp:${TAG:?TAG is required}
    #                                     ^ Fails if TAG is not set
    environment:
      NODE_ENV: ${NODE_ENV:-development}
      LOG_LEVEL: ${LOG_LEVEL:-info}
      DATABASE_URL: postgres://${DB_USER:-postgres}:${DB_PASS:?DB_PASS required}@db:5432/${DB_NAME:-myapp}

  db:
    image: postgres:${PG_VERSION:-16}
    volumes:
      - ${DATA_DIR:-./data}/postgres:/var/lib/postgresql/data

Configuracion multi-entorno: patrones dev/staging/produccion

Los proyectos reales necesitan configuraciones diferentes. Aqui hay patrones probados.

Patron 1: Archivos override (recomendado)

Use un docker-compose.yml base con archivos override especificos del entorno.

# File structure:
project/
  docker-compose.yml              # Base configuration
  docker-compose.override.yml     # Dev overrides (auto-loaded!)
  docker-compose.staging.yml      # Staging overrides
  docker-compose.prod.yml         # Production overrides
# docker-compose.yml (base)
services:
  api:
    image: myapp:${TAG:-latest}
    restart: unless-stopped
    depends_on:
      - db
      - redis

  db:
    image: postgres:16
    volumes:
      - pg-data:/var/lib/postgresql/data

  redis:
    image: redis:7-alpine

volumes:
  pg-data:
# docker-compose.override.yml (development - auto-loaded)
services:
  api:
    build: .
    volumes:
      - .:/app                          # Hot reload
      - /app/node_modules
    ports:
      - "3000:3000"
      - "9229:9229"                     # Debugger
    environment:
      NODE_ENV: development
      LOG_LEVEL: debug
      DB_HOST: db

  db:
    ports:
      - "5432:5432"                     # Expose DB for local tools
    environment:
      POSTGRES_PASSWORD: devpassword    # OK for dev only
# docker-compose.prod.yml (production)
services:
  api:
    # No build, no volumes, no debug port
    ports:
      - "3000:3000"
    environment:
      NODE_ENV: production
      LOG_LEVEL: warn
    env_file:
      - .env.production
    secrets:
      - db_password
      - api_key
    deploy:
      replicas: 3
      resources:
        limits:
          cpus: '1.0'
          memory: 512M

  db:
    # No exposed ports
    environment:
      POSTGRES_PASSWORD_FILE: /run/secrets/db_password
    secrets:
      - db_password
    deploy:
      resources:
        limits:
          cpus: '2.0'
          memory: 1G

secrets:
  db_password:
    file: ./secrets/db_password
  api_key:
    file: ./secrets/api_key
# Usage:

# Development (auto-loads docker-compose.override.yml)
docker compose up

# Staging
docker compose -f docker-compose.yml -f docker-compose.staging.yml up -d

# Production
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d

# Validate merged config before deploying
docker compose -f docker-compose.yml -f docker-compose.prod.yml config

Patron 2: Archivos .env especificos del entorno

Use --env-file para cargar diferentes conjuntos de variables por entorno.

# .env.development
TAG=latest
APP_PORT=3000
DB_PASSWORD=devpassword
LOG_LEVEL=debug
REPLICAS=1

# .env.staging
TAG=rc-1.2.3
APP_PORT=3000
DB_PASSWORD=staging-secret-pw
LOG_LEVEL=info
REPLICAS=2

# .env.production
TAG=1.2.3
APP_PORT=3000
DB_PASSWORD=prod-ultra-secret-pw
LOG_LEVEL=warn
REPLICAS=3

# Usage:
docker compose --env-file .env.development up       # Dev
docker compose --env-file .env.staging up -d         # Staging
docker compose --env-file .env.production up -d      # Production

Patron 3: Profiles para servicios opcionales

Use perfiles Compose para incluir o excluir servicios por entorno.

# docker-compose.yml
services:
  api:
    image: myapp:${TAG:-latest}
    ports:
      - "3000:3000"

  db:
    image: postgres:16
    volumes:
      - pg-data:/var/lib/postgresql/data

  # Dev-only services
  adminer:
    image: adminer
    ports:
      - "8080:8080"
    profiles:
      - dev                           # Only starts with --profile dev

  mailhog:
    image: mailhog/mailhog
    ports:
      - "1025:1025"
      - "8025:8025"
    profiles:
      - dev                           # Only starts with --profile dev

  # Monitoring (staging + production)
  prometheus:
    image: prom/prometheus
    profiles:
      - monitoring                    # Only starts with --profile monitoring

volumes:
  pg-data:

# Usage:
docker compose --profile dev up                     # Includes adminer + mailhog
docker compose --profile monitoring up -d           # Includes prometheus
docker compose --profile dev --profile monitoring up  # Both profiles

Checklist de seguridad: lo que NUNCA debe ir en variables de entorno

Las variables de entorno son convenientes pero inherentemente inseguras para datos sensibles.

NUNCA haga estoHaga esto
Codificar contrasenas en docker-compose.ymlUsar archivos .env (gitignored) o Docker secrets
Commitear archivos .env al control de versionesAgregar .env a .gitignore, commitear .env.example
Usar variables de entorno para claves privadasMontarlas como archivos via volumenes o Docker secrets
Registrar variables de entorno en el codigoEnmascarar valores sensibles en los logs
Compartir archivos .env por Slack o emailUsar un gestor de secretos (Vault, AWS Secrets Manager)
Usar los mismos secretos en dev/staging/produccionGenerar secretos unicos por entorno
# .gitignore — essential entries for Docker projects
.env
.env.*
!.env.example
secrets/
*.pem
*.key
*.crt
# .env.example — commit this as a template
# Database
DB_HOST=localhost
DB_PORT=5432
DB_USER=myapp
DB_PASSWORD=CHANGE_ME

# API Keys
API_KEY=CHANGE_ME
JWT_SECRET=CHANGE_ME

# Application
NODE_ENV=development
LOG_LEVEL=debug
APP_PORT=3000

Depuracion: por que no se carga mi variable de entorno?

Siga este proceso sistematico cuando una variable de entorno no llega a su contenedor.

Paso 1: Esta definida la variable?

Verifique que existe en su archivo .env, env_file o directiva environment.

Paso 2: Llega al contenedor?

Ejecute docker compose exec <servicio> env para ver todas las variables.

Paso 3: Esta el archivo .env en el lugar correcto?

El archivo .env debe estar en el mismo directorio que docker-compose.yml.

Paso 4: Hay un conflicto de nombres?

Verifique errores tipograficos, sensibilidad a mayusculas y precedencia.

Paso 5: Es valido el archivo Compose?

Ejecute docker compose config para ver la configuracion resuelta.

Paso 6: Se esta sobrescribiendo la variable?

Las variables shell del host sobrescriben los valores de .env. Verifique con echo $VAR_NAME.

Comandos de depuracion utiles

# 1. See the fully resolved Compose config (all variables substituted)
docker compose config

# 2. Check environment variables inside a running container
docker compose exec api env

# 3. Check a specific variable
docker compose exec api sh -c 'echo $DATABASE_URL'

# 4. Inspect container config (shows env vars, mounts, etc.)
docker inspect <container_id> --format '{{json .Config.Env}}' | jq .

# 5. Check if .env file is being read
docker compose config --format json | jq '.services.api.environment'

# 6. Verify env_file exists and is readable
docker compose config 2>&1 | grep -i "env_file"

# 7. Run a one-off container to test environment
docker compose run --rm api env | sort

# 8. Check for shell variable conflicts
env | grep -i "DB_"

# 9. Validate Compose file syntax
docker compose config --quiet && echo "Valid" || echo "Invalid"

# 10. Show variable substitution warnings
docker compose --verbose up 2>&1 | grep -i "variable"

Preguntas frecuentes

Cual es la diferencia entre .env y env_file en Docker Compose?

El archivo .env se lee automaticamente para la sustitucion en docker-compose.yml. La directiva env_file carga variables directamente en el entorno del contenedor.

Como paso variables de entorno del host al contenedor?

Liste el nombre de la variable sin valor en la directiva environment o use la sintaxis ${MY_VAR} en docker-compose.yml.

Son seguras las variables de entorno de Docker?

No. Son visibles con docker inspect y aparecen en /proc/environ. Use Docker secrets para datos sensibles.

Cual es el orden de precedencia de las variables de entorno?

De mayor a menor: 1) docker compose run -e, 2) Variables shell del host, 3) Directiva environment, 4) Directiva env_file, 5) Dockerfile ENV.

Como usar variables diferentes para desarrollo y produccion?

Use archivos override: docker-compose.yml base + docker-compose.override.yml para dev + docker-compose.prod.yml para produccion.

Cual es la diferencia entre Docker secrets y variables de entorno?

Los secretos se montan como archivos en /run/secrets/ en lugar de variables de entorno. Son mas seguros y soportan el sufijo _FILE.

𝕏 Twitterin LinkedIn
¿Fue útil?

Mantente actualizado

Recibe consejos de desarrollo y nuevas herramientas.

Sin spam. Cancela cuando quieras.

Prueba estas herramientas relacionadas

🐳Docker Compose GeneratorYMLYAML Validator & Formatter.gi.gitignore Generator

Artículos relacionados

Hoja de referencia Docker Compose: Servicios, volúmenes y redes

Referencia Docker Compose: definiciones de servicios, volúmenes, redes, variables de entorno y ejemplos de stacks.

Guía de archivos .env: Mejores prácticas de variables de entorno

Domina archivos .env y variables de entorno.