Docker Secrets in Compose and Swarm

Khimananda Oli 9 min read Database
Docker Secrets in Compose and Swarm

By Khimananda Oli | Last reviewed: August 2026

Hardcoding credentials or passing them as plain environment variables remains one of the most common security failures I see in containerized applications. Properly implementing Docker Secrets in Compose and Swarm eliminates this risk by injecting sensitive data as in-memory files rather than visible process arguments. This approach keeps database passwords, API keys, and TLS certificates out of your image layers, shell history, and inspection logs.

For teams transitioning from single-host development to orchestrated production environments, understanding the distinction between local secret emulation and true Swarm-managed secrets is critical. While Docker Compose multi-container setups allow you to mimic secrets using local files for developer convenience, only Swarm mode provides the encrypted Raft log storage and RBAC enforcement required for compliance. If you are building systems that must pass SOC 2 or ISO 27001 audits, relying solely on environment variables will fail control tests; you need the cryptographic guarantees that native secrets provide.

Manager NodeEncrypted Raft LogSecrets Store (AES)Worker NodeContainer Runtime/run/secrets/db_passApplicationRead File OnlyNo ENV ExposureTLS Mutual Authtmpfs Mount
Docker Secrets architecture: encrypted storage on managers flows over mutual TLS to worker nodes, where secrets are mounted as tmpfs files never touching disk.

How do Docker Secrets in Compose and Swarm differ from environment variables?

Environment variables are the default for many developers because they are simple, but they are fundamentally insecure for sensitive data. When you pass DB_PASSWORD=mysecret via ENV or -e, that value becomes visible in multiple dangerous locations: docker inspect output, process listings (/proc/<pid>/environ), crash dumps, child process inheritance, and CI/CD logs. In audit scenarios, any tool that captures container metadata inadvertently captures your credentials.

Docker Secrets solve this by treating sensitive data as first-class objects managed by the orchestrator. In Swarm mode, secrets are encrypted at rest using the cluster’s Raft consensus log. They are transmitted only over mutual TLS connections between nodes and mounted into containers as temporary files in a memory-backed filesystem (tmpfs). The application reads the file content at runtime; the secret never appears in the container specification, environment block, or image layers.

Security comparison matrix

CriterionEnvironment VariablesDocker Secrets (Swarm)Compose Secrets (Local)
Visibility in inspectFull plaintext exposureReference only, no valueFile path reference only
Process listing safetyExposed via /proc/environSafe (file read only)Safe (file read only)
Encryption at restNoneAES-256 in Raft logPlaintext file on host
Image layer safetyBaked in if set in DockerfileNever in imageNever in image
RBAC supportNoneNode + service levelNone (single host)
Audit complianceFails most controlsPasses SOC2/ISO27001Development only

The trade-off is operational complexity. Secrets require an initialized Swarm cluster and application code that can read from files instead of expecting environment variables. For legacy applications that strictly require env vars, you can use an entrypoint script to read the secret file and export it, though this partially defeats the security benefit since the value then exists in the process environment.

How do you configure Docker Secrets in Compose for local development?

Local development rarely runs a full Swarm cluster, but you still want parity with production secret handling. Docker Compose (V2) supports a secrets top-level element that emulates Swarm behavior using bind mounts. This lets developers test secret-reading logic without deploying to a cluster. Note that these are not encrypted; they are convenience mappings for local workflow only.

# docker-compose.yml
version: "3.9"

services:
  api:
    image: myapp:latest
    secrets:
      - db_password
      - jwt_signing_key
    environment:
      # Tell the app WHERE to find secrets, not the values
      DB_PASSWORD_FILE: /run/secrets/db_password
      JWT_KEY_FILE: /run/secrets/jwt_signing_key

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

In this configuration, Compose creates a bind mount from your local ./secrets/ directory to /run/secrets/ inside the container. Your application must be coded to check for _FILE suffixed environment variables or directly read from the standard path. This pattern aligns with the Kubernetes secrets management conventions many teams adopt later.

Critical local security practices

  • Never commit secret files: Add secrets/ to your .gitignore immediately. Provide secrets/*.example templates instead.
  • Restrict file permissions: Set local secret files to chmod 600 so only your user can read them.
  • Use unique dev values: Never reuse staging or production credentials locally. Compromised dev machines should not endanger other environments.
  • Document the contract: Your README should list every expected secret file and its format so new developers can bootstrap without guessing.

How do you create and manage secrets in a Docker Swarm cluster?

Production Swarm clusters handle secrets through the Docker CLI or API. Secrets are immutable; you cannot update a secret’s content. Instead, you create a new versioned secret and update the service to reference it. This immutability is intentional—it provides an audit trail and prevents silent credential rotation failures.

# Create a secret from stdin (preferred for automation)
echo "SuperSecureP@ss2026!" | docker secret create db_password_v3 -

# Create a secret from a file
docker secret create tls_cert ./certs/server.crt

# List all secrets (values are NEVER shown)
docker secret ls

# Inspect metadata only
docker secret inspect db_password_v3

# Remove old secret versions after rotation
docker secret rm db_password_v2

When creating secrets, avoid passing values as command-line arguments whenever possible. Shell history captures CLI arguments, defeating the purpose of using secrets. Use stdin piping, file sources, or integrate with external secret stores like HashiCorp Vault via driver plugins for production-grade workflows. For teams managing secrets in CI/CD pipelines, always use masked variables and pipe mechanisms rather than literal string parameters.

1. Create Newdb_password_v3Encrypted in Raft2. Update Service--secret-rm v2--secret-add v33. Rolling DeployTasks RestartZero Downtime4. Cleanup Olddocker secret rm v2After VerificationService Update Commanddocker service update \--secret-rm db_password_v2 \--secret-add source=db_password_v3,target=db_password \--update-parallelism 1 --update-delay 10s \myapp_api
Immutable secret rotation workflow: create new version, update service with rolling restart, verify health, then remove deprecated secret.

Granting secrets to services with least privilege

Not every service needs every secret. Explicitly declare which services consume which secrets in your stack file or CLI commands. Swarm enforces this at the scheduler level—a task scheduled on a node only receives the secrets declared for its service.

# In docker-stack.yml
services:
  api:
    image: myapp:latest
    secrets:
      - source: db_password_v3
        target: db_password
        uid: "1000"
        gid: "1000"
        mode: 0400  # Read-only for app user
  worker:
    image: myapp-worker:latest
    secrets:
      - redis_auth  # Worker gets different secrets
  frontend:
    image: nginx:alpine
    # No secrets declared = no access

Setting explicit uid, gid, and mode ensures the secret file is readable only by the application process, not by other users or processes in the container. This defense-in-depth matters when running containers as non-root, which you should always do in production.

How do you troubleshoot missing or inaccessible Docker Secrets?

Secret issues manifest as application startup failures or permission errors. Because secrets are designed to be opaque, debugging requires methodical verification rather than inspecting values directly.

  1. Verify secret existence: Run docker secret ls on a manager node. If the secret is missing, recreate it. Remember that secrets are cluster-scoped, not namespace-scoped like Kubernetes.
  2. Check service attachment: Run docker service inspect <name> --pretty and look under Secrets. Confirm the source name matches exactly—typos silently fail.
  3. Validate mount path: Exec into a running task: docker exec -it <task_id> ls -la /run/secrets/. Verify the file exists with correct ownership and permissions.
  4. Test read access: Run cat /run/secrets/<name> inside the container. If permission denied, check UID/GID mapping against your container’s runtime user.
  5. Review service logs: Application errors often reveal whether the issue is a missing file versus unreadable content. Look for "file not found" versus "permission denied" distinctions.
  6. Confirm node health: If tasks are pending, run docker node ls. Unreachable workers cannot receive secret distributions from managers.

A frequent mistake is assuming secrets propagate instantly. After updating a service, existing tasks continue using old secrets until they are replaced. Use --update-parallelism and --update-delay flags to control rollout speed and allow health checks to validate each new task before proceeding.

When should you use external secret drivers instead of native Docker Secrets?

Native Docker Secrets work well for small-to-medium clusters, but they have limitations at scale. The Raft log has practical size limits, secret rotation requires manual orchestration, and there is no built-in integration with cloud KMS or HSM backends. For enterprises managing hundreds of services across multiple clusters, external secret drivers provide centralized policy, automatic rotation, and audit logging.

Need Secrets?>50 Services OR Multi-Cluster?NOYESNative Docker Secrets✓ Simple setup✓ Zero dependenciesExternal Driver (Vault/AWS)✓ Auto-rotation✓ Centralized auditBest: Startups, Single ClusterBest: Enterprise, Compliance
Decision framework: choose native Docker Secrets for simplicity and small scale; migrate to external drivers when compliance, rotation, or multi-cluster sync becomes mandatory.

Popular external drivers include HashiCorp Vault, AWS Secrets Manager, and Azure Key Vault. These integrate via Docker’s secret driver plugin interface, making the consumption pattern identical to native secrets from the application’s perspective. The operational overhead increases—you now manage another distributed system—but the security and compliance benefits justify it for regulated workloads. Teams pursuing HashiCorp Vault integration gain dynamic secrets, lease-based expiration, and detailed access auditing that native Swarm cannot provide.

Implementing Secure Secrets Management Today

Start by auditing your current containers for exposed environment variables. Migrate high-risk credentials (database passwords, payment API keys, signing certificates) to Docker Secrets in Compose and Swarm first. Update your application code to support file-based secret reading alongside env var fallbacks for backward compatibility. Establish a rotation runbook that documents the immutable versioning process, and test it quarterly. For teams operating beyond a single cluster or requiring automated compliance evidence, plan an external secret driver migration as a dedicated project. If you need guidance on architecting a secrets strategy that balances developer velocity with audit readiness, reach out to discuss your infrastructure.

Frequently Asked Questions

Define secrets under the top-level secrets key, referencing a local file or external source. Then grant access to specific services using the secrets list within that service definition. This keeps sensitive data out of environment variables and image layers while maintaining declarative configuration for your stack.

Standalone Compose mounts secrets as read-only files from the host at runtime without encryption. Swarm mode encrypts secrets at rest and in transit across the cluster, storing them in the Raft log. Swarm also supports rotating secrets without redeploying services, unlike standard Compose which requires container restarts.

No, environment variables are visible in process lists, logs, and inspect commands. Secrets mount as tmpfs files with restricted permissions, preventing accidental exposure. Always prefer secrets for credentials, API keys, and certificates in both Compose and Swarm environments to maintain proper security boundaries.

Swarm stores encrypted secrets in the Raft consensus log on manager nodes. At runtime, they exist only in memory-backed tmpfs filesystems inside containers. They never touch the container writable layer or host disk unencrypted, ensuring protection even if the node storage is compromised or stolen.

Create a new secret version with a different name, update the service to reference it, then remove the old secret after deployment completes. Use docker service update --secret-rm and --secret-add flags atomically. Rolling updates ensure zero downtime while transitioning workloads to the updated credential safely.

Yes, Compose V2 supports the secrets syntax but treats them as bind mounts from local files rather than true encrypted secrets. The file path must exist on the host running docker compose up. For production-grade secret management with encryption, deploy using Swarm mode or an external secrets driver.

Default permissions are 0444 owned by root. Override this using uid, gid, and mode parameters in the service secrets configuration. Set restrictive modes like 0400 for private keys and specify non-root UIDs when applications run as unprivileged users to follow least-privilege principles correctly.

Yes, configure third-party drivers via the external option in the secrets definition. Supported backends include HashiCorp Vault, AWS Secrets Manager, and Azure Key Vault through dedicated plugins. The driver fetches secrets at service creation time, allowing centralized policy enforcement and audit logging across all Swarm nodes.

Verify the secret exists using docker secret ls and matches the name in your Compose file exactly. Check that the source file path is correct for standalone Compose or that the secret was created in the same Swarm cluster. Missing references cause immediate container startup failures during orchestration validation.

No. Standalone Compose reads plaintext files directly from the host filesystem and mounts them into containers. Encryption only occurs in Swarm mode where managers protect secrets in the Raft store. For standalone deployments requiring encryption, use an external secrets driver or pre-encrypt files before mounting them.

Exec into the container and cat the mounted file at /run/secrets/name to verify content. Check source file encoding for hidden characters or trailing newlines. Confirm the secret was created with docker secret create using stdin redirection rather than echo, which may introduce unwanted whitespace or formatting issues.

Yes, reference the same top-level secret definition across multiple service blocks. Each service receives its own isolated tmpfs mount pointing to identical content. Updates require creating a new secret version and updating all dependent services simultaneously to maintain consistency across the entire application stack.

Encrypted secrets persist in the Raft log replicated across all manager nodes. When workers reconnect or replacements join, managers redistribute decrypted secrets securely over TLS. Orphaned containers lose access immediately upon termination since secrets exist only in ephemeral memory, preventing credential leakage from crashed or decommissioned nodes.

Swarm enforces a 500KB maximum per secret including metadata. Exceeding this causes service creation failures. Split large configurations into multiple secrets or use external object storage with signed URLs referenced via smaller secrets. Compose standalone has no hard limit but practical constraints apply based on available tmpfs space.

List all secrets with docker secret ls and identify unreferenced ones by checking service configurations. Remove orphaned secrets using docker secret rm to prevent credential sprawl. Never delete secrets still referenced by active services as this breaks deployments. Automate cleanup in CI pipelines to maintain hygiene across environments.