Database passwords, API keys and TLS keys have to reach your containers somehow, and some ways are much safer than others. Secrets baked into images, committed to Git, or visible in docker inspect are common sources of leaks. Here’s how to handle them properly with plain Docker and Compose.

General principles: secrets management.

Where secrets leak

Mistake Why it’s bad
ENV PASSWORD=… or COPY .env in a Dockerfile Stored in image layers. Anyone with the image can read it, even if a later layer deletes it
Secrets in compose.yaml committed to Git Git history is forever, and repositories get shared
Build args for secrets (--build-arg TOKEN=…) Visible in image history (docker history)
Environment variables Visible in docker inspect, /proc/<pid>/environ, crash reports, and sometimes logs
Logging configuration objects Apps sometimes print their whole config at startup

Level 1: .env files (simple, common)

services:
  app:
    image: myapp:1.8.3
    env_file: .env
chmod 600 /opt/app/.env
echo ".env" >> .gitignore

Acceptable for many small deployments, if the file is protected and never committed. Variables remain visible to anyone with Docker access on the host (who effectively has root anyway). See Docker environment variables.

Level 2: Compose secrets (files mounted into the container)

services:
  db:
    image: postgres:16.4
    environment:
      POSTGRES_PASSWORD_FILE: /run/secrets/db_password
    secrets:
      - db_password
  app:
    image: myapp:1.8.3
    secrets:
      - db_password
      - api_key

secrets:
  db_password:
    file: ./secrets/db_password.txt
  api_key:
    file: ./secrets/api_key.txt
  • Each secret appears as a file in /run/secrets/<name> inside the container. It’s not an environment variable, so it doesn’t show up in docker inspect.
  • Many official images support *_FILE variables (POSTGRES_PASSWORD_FILE, MYSQL_ROOT_PASSWORD_FILE, …) to read secrets from files.
  • Your own app reads the file at startup.
mkdir -p secrets && chmod 700 secrets
openssl rand -base64 32 > secrets/db_password.txt
chmod 600 secrets/*

In Docker Swarm, secrets are stored encrypted in the cluster, and only delivered to services that need them (docker secret create). See Docker Swarm.

Build-time secrets (private packages, tokens)

Never pass tokens as build args. Use BuildKit secret mounts. They’re available only during that RUN step, and not stored in layers:

# syntax=docker/dockerfile:1
RUN --mount=type=secret,id=npm_token \
    NPM_TOKEN=$(cat /run/secrets/npm_token) npm ci
docker build --secret id=npm_token,src=$HOME/.npm_token -t myapp .

For private Git repositories, use RUN --mount=type=ssh with docker build --ssh default. See Dockerfile tutorial and multi-stage builds.

Check your images

docker history --no-trunc myapp:1.8.3 | grep -iE 'key|token|pass'
docker run --rm -it myapp:1.8.3 env

Scanners such as Trivy can detect embedded secrets. See Trivy image scanning.

Bigger setups: a secrets manager

For many services or servers, use a dedicated secrets manager (HashiCorp Vault/OpenBao, Infisical, Doppler, cloud secret managers, or Bitwarden Secrets Manager), and inject secrets at deploy time. Kubernetes has its own Secret objects (encrypt them at rest). See Kubernetes ConfigMaps and Secrets.

Rotation

  • Use different secrets per environment (staging ≠ production).
  • Rotate after staff changes or suspected leaks: update the secret file, then recreate the containers (docker compose up -d --force-recreate app).
  • If a secret ever reached Git, assume it’s compromised: rotate it. Rewriting history isn’t enough.

Quick reference

Need Approach
Simple runtime config .env with chmod 600, in .gitignore
Passwords for official images Compose secrets: + *_FILE variables
Tokens during build RUN --mount=type=secret
Swarm docker secret create
Many services Secrets manager

Related: Docker security best practices and Docker Compose in production.

  • Docker
  • Security
  • DevOps