← Back to blogs

Docker Compose Secrets: A Practical Guide for 2026

May 25, 2026CloudCops

docker compose secrets
docker security
secrets management
devops
ci/cd
Docker Compose Secrets: A Practical Guide for 2026

You usually find the secret problem by accident.

A developer opens a repository to fix a bug and spots a .env file with a database password. Someone else checks old commits and finds an API token that was “temporary” six months ago. A CI job starts failing, so a teammate prints environment variables for debugging and suddenly a credential is sitting in build logs. None of this happens because teams are careless. It happens because shipping pressure is real, local setup needs to be fast, and secrets are often treated like configuration until they become an incident.

Docker Compose gives teams a better first move. Docker Compose secrets won't solve the entire secrets lifecycle on their own, but they do solve a very common and very expensive class of mistakes: putting sensitive values in environment variables, image layers, and source-controlled config. That makes them useful for local development, internal environments, and as a stepping stone toward a more mature secret delivery model in CI/CD and production.

The All-Too-Common Secret in Your Git History

The pattern is familiar. A team starts with a simple stack: app, database, cache. Local development needs to work quickly, so credentials go into .env. Then the same values get copied into a deployment script, then into a wiki page, then into a CI variable with a slightly different name. A few weeks later, nobody knows which secret is authoritative, which one is stale, or who still has access.

The technical problem is obvious, but the operational problem is worse. Once a secret lands in Git history, it tends to stick around in places people forget to clean up. Cloned repositories, CI logs, shell history, old screenshots, copied snippets in tickets. Even when teams rotate the value, they often don't fix the workflow that leaked it in the first place.

Why teams keep ending up here

Most secret leaks in Docker projects don't start with malice or negligence. They start with convenience.

  • Fast local setup: A .env file feels easy when the team just wants docker compose up to work.
  • Legacy app assumptions: Many applications still expect credentials as environment variables, so teams keep feeding them that way.
  • Mixed ownership: Platform engineers, app developers, and CI maintainers each touch a different piece of the secret path.
  • No lifecycle thinking: Teams decide where to put the secret, but not how to rotate it, scope it, or remove it safely.

A secret handling pattern isn't secure just because it avoids hardcoding. It's secure when the whole path from storage to runtime is controlled.

Docker Compose secrets are a practical correction for this. They move sensitive values out of the most leak-prone places and force a more deliberate access model. Instead of globally available environment variables, you define a secret once and grant it only to the services that need it. That doesn't eliminate all risk, but it shrinks the blast radius immediately.

What a good first fix looks like

A strong first step isn't “buy a vault.” It's usually much simpler:

  1. Stop storing secrets in app config files that developers casually edit and commit.
  2. Move secrets to file-based mounts so containers read them from the filesystem at runtime.
  3. Grant access per service instead of exposing every credential to every container.
  4. Treat local dev and production differently without maintaining totally different application logic.

That shift is where Docker Compose secrets help. They give you a built-in mechanism that is good enough to clean up common bad habits and structured enough to support a later move to Vault, SOPS, or a cloud secret manager.

How to Define and Use Docker Compose Secrets

Docker designed Compose secrets to keep sensitive values out of environment variables and image layers. In Docker's Compose secrets documentation, secrets are defined in the top-level secrets element, explicitly granted to services, and mounted inside the container at /run/secrets/<secret_name>. That file-based model matters because environment variables are often exposed in logs or debugging output.

A hand-drawn illustration of a person coding Docker Compose secrets configuration on a laptop screen.

A clean mental model helps. You don't “inject a password into Docker Compose.” You declare a secret source, then authorize specific services to see it. Inside the container, the application reads a file.

A working Compose example

Here's a minimal pattern with an app and a PostgreSQL service:

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

  db:
    image: postgres:16
    secrets:
      - db_password
    environment:
      POSTGRES_PASSWORD_FILE: /run/secrets/db_password

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

This does two useful things.

First, the secret is declared once under secrets. Second, only app and db can read it because only those services are granted access. That service-level grant is where Compose becomes more disciplined than the usual .env sprawl.

For teams tightening their container posture more broadly, this file-based pattern fits well with other Docker security practices such as minimizing runtime exposure and reducing accidental credential handling.

Reading the secret inside the application

Your application has to meet Docker Compose halfway. If it only reads DB_PASSWORD from the environment, you either need a wrapper script or support for file-based reads.

A simple Python example looks like this:

from pathlib import Path
import os

def read_secret(name: str) -> str:
    path = Path(f"/run/secrets/{name}")
    if path.exists():
        return path.read_text().strip()
    return os.getenv(name.upper(), "")

db_password = read_secret("db_password")

That fallback is useful for development and tests, but the runtime target should be the mounted file.

What actually gets mounted

Inside the container, Docker Compose mounts the secret at:

/run/secrets/db_password

That detail matters operationally. The app doesn't need to know where the original file came from on the host. It only needs a stable in-container path.

A lot of official images already support this pattern through _FILE environment variables. PostgreSQL is a familiar example. Instead of setting POSTGRES_PASSWORD, you point POSTGRES_PASSWORD_FILE at the mounted secret path. That avoids writing custom bootstrap logic for common services.

Later, when you're wiring this into automation, the same file path becomes the bridge between a secret stored elsewhere and a container that only needs a file at runtime.

Here's a walkthrough if you want to see the pattern in action before adapting it to your own stack:

Practical rule: If a service can consume a secret via _FILE, use that path instead of translating the secret back into a normal environment variable.

Runtime Provisioning for Better Security

Defining a secret in compose.yaml is the easy part. The more important question is where the secret lives before docker compose up runs.

The safest practical answer for many teams is an external file that never enters source control. Compose then references that file, mounts it into the container, and keeps the application logic simple. This doesn't turn Compose into a full secret manager, but it does separate code from sensitive material.

Keep secret material out of the repository

A common layout looks like this:

project/
  compose.yaml
  secrets/
    db_password.txt
    api_key.txt

And in compose.yaml:

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

The non-negotiable part is .gitignore. If the file that feeds a Compose secret is committed, you haven't solved the core problem. You've only moved it.

A practical .gitignore entry is simple:

secrets/

You should also make sure the team knows these files are local runtime artifacts, not configuration that belongs in a pull request.

Why file mounts are still better than env vars

In Wiz's explanation of Docker secrets, Docker Swarm secrets are stored encrypted and mounted into containers on a memory-backed filesystem (tmpfs) that disappears when the container stops. Standalone Compose doesn't provide that same encryption-at-rest model, but the file-based injection pattern still follows the same security idea: avoid persistent plaintext in the wrong places and limit access to explicitly authorized services.

That distinction matters. Compose secrets are safer transport into a running service, not automatic proof of enterprise-grade secret governance.

If you hand Compose a plaintext file from your laptop filesystem, the mounted secret may be safer than an environment variable, but the original source file still needs protection.

Use the _FILE convention whenever possible

A lot of friction with Docker Compose secrets comes from application expectations. Legacy apps often want DB_PASSWORD, API_TOKEN, or JWT_SECRET directly in the environment. That's where _FILE support helps.

Examples:

  • Database containers: POSTGRES_PASSWORD_FILE=/run/secrets/db_password
  • Custom applications: DB_PASSWORD_FILE=/run/secrets/db_password
  • Bootstrap scripts: Read the file path first, then fall back only when needed

A straightforward shell entrypoint pattern looks like this:

export DB_PASSWORD="$(cat "$DB_PASSWORD_FILE")"
exec ./start-app

That isn't perfect, because it turns the file value back into an environment variable for the child process, but it can be a workable bridge for software you can't easily change. If you do this, keep the translation local to startup and avoid logging or debugging commands that echo the value.

What works in practice

Teams usually get the best result from a few habits used together:

  • Store secret files outside normal config folders so developers don't casually browse and edit them.
  • Mount them read-only and grant them only to the containers that need them.
  • Use separate secret files per environment instead of reusing one “temporary” set across dev, staging, and production.
  • Document the expected file names so local setup doesn't devolve into tribal knowledge.

Compose is strongest when you treat it as the last hop into runtime, not the master system of record.

Integrating Docker Secrets into CI/CD Pipelines

The local development pattern breaks down quickly in CI unless you design the handoff properly. Build runners are ephemeral, logs are noisy, and teams often blur the line between runtime secrets and build credentials. That's where many “secure” Compose setups regress back to echo $PASSWORD hacks.

Modern pipelines need a cleaner split. Runtime secrets should arrive when services start. Build-time secrets should be available only during image creation. Those are different problems and they should stay different.

A practical CI flow

A workable pipeline pattern looks like this:

  1. The repository contains compose.yaml with secret references only.
  2. The CI platform stores the actual values in its native secret store.
  3. During the job, the runner writes each value to a temporary file.
  4. docker compose uses those files as secret sources.
  5. The job removes temporary files after deployment or test execution.

That keeps sensitive values out of Git while still satisfying Compose's file-based contract.

A six-step diagram illustrating the process of securely managing secrets within a CI/CD pipeline environment.

For teams refining broader delivery workflows, the same pattern fits neatly into mature CI/CD pipeline design, especially when you want reproducible deploy steps without baking secrets into artifacts.

Example pipeline behavior

Suppose GitHub Actions, GitLab CI, or another runner stores DB_PASSWORD and API_KEY in its own secret store. The job can create runtime files just before Compose starts:

mkdir -p secrets
printf '%s' "$DB_PASSWORD" > secrets/db_password.txt
printf '%s' "$API_KEY" > secrets/api_key.txt

docker compose up -d

After the job completes, remove the files:

rm -f secrets/db_password.txt secrets/api_key.txt

The important detail isn't the shell syntax. It's the control point. The secret value lives in the CI platform until the runner materializes it briefly for runtime consumption. The repository never holds the actual credential.

Build-time and runtime are not the same

Teams often combine these concerns. According to the BuildKit guidance summarized in this Docker secret workflow discussion, modern DevOps pipelines need to treat build-time and runtime secrets as different threat models. Docker Compose secrets handle runtime credentials, while BuildKit's --secret flag is meant for build-time secrets such as private package repository tokens, without leaking them into final image layers.

That means:

  • Use BuildKit secrets when a Dockerfile needs temporary access during docker build.
  • Use Docker Compose secrets when a running service needs a credential after the container starts.
  • Don't use either as a substitute for lifecycle management such as rotation, auditability, and revocation.

Build-time secrets belong to image construction. Runtime secrets belong to live services. If one mechanism is doing both jobs, the design usually needs another pass.

Pipeline mistakes worth avoiding

The failures are predictable.

  • Printing environment state for debugging: This often leaks credentials into logs.
  • Writing secret files into the workspace and caching them: CI caches outlive the job that created them.
  • Passing secrets through build args: That invites leakage into image history or layered artifacts.
  • Using the same static secret everywhere: Local dev, CI, staging, and production shouldn't all depend on one credential.

A good pipeline doesn't just “have secrets.” It narrows exposure at each stage and leaves as little behind as possible when the job exits.

When to Upgrade to Vault, SOPS, or Cloud KMS

Docker Compose secrets are useful, but they have a ceiling. They improve how secrets reach containers. They do not, by themselves, answer the harder questions: Who changed this secret? Who accessed it? How do we rotate it cleanly? How do we keep plaintext out of repositories and laptops? How do we issue short-lived credentials instead of static ones?

That's why production teams should treat Compose as a delivery mechanism, not the entire system. GitGuardian's guidance on handling secrets in Docker recommends pairing Compose with an external manager like Vault or an encrypted-file workflow like SOPS when you need rotation, auditing, and lifecycle controls.

Where Compose starts to fall short

Compose secrets are often enough when:

  • A small team runs a limited number of services
  • Local development needs a safer default
  • The immediate goal is removing secrets from env files and image layers

They start to struggle when:

  • You need centralized audit trails
  • Security or compliance teams require formal access controls
  • Secret rotation has to happen on a schedule
  • Multiple environments need a single source of truth
  • You want dynamic credentials instead of long-lived passwords

At that point, the question isn't whether Compose is “bad.” It's whether your secret lifecycle now requires a control plane.

Secrets management tool comparison

FeatureDocker Compose SecretsSOPS (Git-based)Cloud KMS/Secret ManagerHashiCorp Vault
Primary roleRuntime secret delivery to servicesEncrypt secret files for Git workflowsManaged secret storage and delivery in a cloud ecosystemCentralized secret management with advanced policy controls
Best fitLocal stacks, smaller deployments, service-scoped mountsGitOps teams that want encrypted secrets in repositoriesTeams already deep in AWS, Azure, or Google CloudPlatforms needing dynamic secrets, strict policies, and broad integrations
Rotation supportExternal process requiredExternal process requiredUsually handled by the managed platform and surrounding automationStrong option when teams need formal rotation workflows
AuditabilityLimited by itselfDepends on Git history plus surrounding controlsBetter fit when cloud-native auditing is requiredStrong fit when access logging and policy enforcement matter
Developer experienceSimple to startGood for repo-centric operationsSmooth if workloads already live in one cloudPowerful, but heavier to operate well
Plaintext risk in GitAvoided only if source files stay out of repoReduced through encrypted files in repoAvoided when values stay in the managerAvoided when values stay in Vault and are fetched at runtime
Operational overheadLowModerateModerate, often lower than self-hosted systemsHigher, especially for self-managed deployments

How to choose without overengineering

Choose Docker Compose secrets when your current problem is straightforward secret leakage in local dev or simple deployments. If credentials are still sitting in .env or copied into Compose files, fix that first.

Choose SOPS when your team is already committed to GitOps and wants encrypted configuration in the repository. This works well when developers need reviewable changes without exposing plaintext.

Choose a cloud secret manager or KMS-backed service when your workloads already live primarily on one cloud provider. The tighter integration can reduce operational drag. Cost and ecosystem fit matter here, and if you're weighing broader cloud trade-offs at the same time, this guide to compare cloud provider pricing is useful context.

Choose Vault when secret management itself is now part of the platform. Vault makes sense when teams need dynamic credentials, detailed policy control, stronger separation of duties, and a consistent model across mixed environments.

A decision rule that holds up well

If the main challenge is delivery, Compose can still do the job.

If the main challenge is governance, Compose isn't enough.

That distinction saves teams a lot of churn. Many startups don't need Vault on day one. Many regulated or multi-team platforms outgrow Compose-native handling much sooner. The right move depends less on company size than on operational complexity, audit pressure, and how much damage a leaked static credential could cause.

For teams handling regulated data or building a broader cloud security architecture, secret handling should also align with the rest of your cloud encryption strategy, not sit beside it as an isolated workaround.

Your Migration Path to Mature Secrets Management

It's often premature to jump from .env files straight to a full enterprise vault rollout. That's too much process for a problem that often starts with basic hygiene. A phased path works better because each step removes a real class of risk without forcing the entire organization to retool overnight.

Start with Docker Compose secrets. Move credentials out of source-controlled config and environment-variable sprawl. Make applications read from mounted files where possible. That alone cleans up a surprising amount of accidental exposure.

Then tighten the delivery path. Feed Compose from your CI platform's native secret store and create temporary files only during the job. Separate build-time and runtime credentials so tokens used during image builds don't leak into artifacts or get reused by running services.

After that, decide whether your bottleneck is still delivery or whether it's become governance. If you're struggling with rotation, audit requests, environment sprawl, or shared ownership across teams, it's time to move the source of truth into Vault, SOPS, or a cloud-managed secret service. Compose can remain the last-mile runtime mechanism while the upstream system handles policy and lifecycle.

A five-step roadmap for maturing secrets management, from Docker Compose to zero-trust ephemeral secrets architecture.

The key is to treat secret management as an operating model, not a YAML trick. Teams mature when they stop asking “where do we store this password?” and start asking “how does this secret get created, delivered, rotated, observed, and revoked across every environment we run?”


If your team needs help designing that path, CloudCops GmbH works with startups, scale-ups, and enterprise platform teams to build secure cloud-native delivery workflows, harden CI/CD, and implement practical secrets management patterns that fit real operating environments.

Ready to scale your cloud infrastructure?

Let's discuss how CloudCops can help you build secure, scalable, and modern DevOps workflows. Schedule a free discovery call today.

Continue Reading

Read Zero Downtime Deployment Strategies: A Practitioner's Guide
Cover
Apr 25, 2026

Zero Downtime Deployment Strategies: A Practitioner's Guide

Learn practitioner-focused zero downtime deployment strategies. This guide covers blue-green, canary, and GitOps for modern, cloud-native applications.

zero downtime deployment
+4
C
Read Mastering Docker Build Args for Better Container Builds
Cover
Mar 21, 2026

Mastering Docker Build Args for Better Container Builds

Unlock the power of docker build args. This guide shares expert strategies for creating flexible, secure, and blazing-fast container builds.

docker build args
+4
C
Read Mastering The Pipeline In Jenkins For Modern CI/CD
Cover
Mar 18, 2026

Mastering The Pipeline In Jenkins For Modern CI/CD

Discover how a pipeline in Jenkins transforms software delivery. This guide explains Declarative vs. Scripted, Jenkinsfiles, and cloud native workflows.

pipeline in jenkins
+4
C