Docker Swarm can manage secrets, but only if you treat them like any other piece of production data and use a dedicated secrets management system like HashiCorp Vault.

Let’s see how this actually plays out. Imagine a web application running on Swarm. It needs a database password and an API key.

# docker-compose.yml (simplified)
version: '3.8'
services:
  webapp:
    image: my-webapp:latest
    secrets:
      - db_password
      - api_key
    environment:
      DB_PASSWORD_FILE: /run/secrets/db_password
      API_KEY_FILE: /run/secrets/api_key

secrets:
  db_password:
    external: true
  api_key:
    external: true

In this docker-compose.yml, external: true tells Swarm that these secrets aren’t defined within the compose file itself but will be provided externally. Swarm will then mount these secrets as files inside the webapp container at /run/secrets/. The application code reads these files to get the sensitive data.

The "external" part is crucial. Swarm itself can store secrets, but it’s not designed for robust, audited, and versioned secrets management. It’s more of a basic encryption and distribution mechanism. For production, you need something more.

This is where HashiCorp Vault shines. Vault acts as a central, secure "key-value store" for secrets. It can encrypt secrets at rest and in transit, provides fine-grained access control, and audits every access.

Here’s the flow:

  1. Vault Setup: You install and configure Vault. You define "secrets engines" (like Key/Value v2) and store your secrets. For example, you might store db_password under secret/data/myapp/database and api_key under secret/data/myapp/api.

  2. Swarm Integration: You need a way for Swarm services to get these secrets from Vault. The most common and secure method is to use a sidecar container or an init container that’s specifically designed to fetch secrets from Vault and make them available to your main application container. A popular tool for this is vault-agent or relayd (from the Vault ecosystem).

    Let’s consider using vault-agent as a sidecar. Your docker-compose.yml would look something like this:

    # docker-compose.yml with Vault integration
    version: '3.8'
    services:
      webapp:
        image: my-webapp:latest
        secrets:
          - db_password_file # This will be a file created by the sidecar
          - api_key_file     # This will be a file created by the sidecar
        environment:
          DB_PASSWORD_FILE: /run/secrets/db_password_file
          API_KEY_FILE: /run/secrets/api_key_file
    
      vault-agent-sidecar:
        image: hashicorp/vault-agent:latest
        volumes:
          - vault-agent-config:/vault/config # Mount the configuration
          - secrets-store:/run/secrets        # Mount a shared volume for secrets
        environment:
          # Environment variables to configure the agent (e.g., Vault address, auth method)
          VAULT_ADDR: "https://vault.mydomain.com:8200"
          VAULT_ROLE_ID: "your-role-id" # Example: AppRole auth
          VAULT_SECRET_ID: "your-secret-id" # Example: AppRole auth
    
    volumes:
      vault-agent-config:
        driver: local
      secrets-store:
        driver: local
    
    secrets:
      db_password_file:
        file: /path/to/dummy/db_password.txt # This is a placeholder, the real secret comes from the sidecar
      api_key_file:
        file: /path/to/dummy/api_key.txt     # This is a placeholder
    

    The vault-agent-sidecar service would have a configuration file (mounted via vault-agent-config) that tells it:

    • Where Vault is (VAULT_ADDR).
    • How to authenticate (e.g., using AppRole, Kubernetes auth, etc.).
    • Which secrets to fetch (e.g., secret/data/myapp/database and secret/data/myapp/api).
    • Where to write these fetched secrets (e.g., into the shared secrets-store volume, which is then accessible by the webapp service via the secrets-store volume mount).

    The webapp service declares the secrets it needs, but instead of pointing to Swarm secrets, it points to files that the vault-agent-sidecar will create in the shared volume.

  3. Secret Rendering: The vault-agent authenticates with Vault, reads the specified secrets, and writes them as files (e.g., db_password_file, api_key_file) into the /run/secrets directory within the shared volume.

  4. Application Access: The webapp service then reads these files from /run/secrets just as it would with Swarm-managed secrets.

The key insight here is that Swarm becomes the orchestrator of your application, and Vault becomes the authority for your secrets. Swarm’s role secrets are minimal; they are just encrypted blobs that Swarm distributes. Vault, on the other hand, provides the robust security, auditing, and lifecycle management your production secrets demand.

When you deploy this, Swarm will start both the webapp and the vault-agent-sidecar services. The sidecar will run first, fetch the secrets, and populate the shared volume. Then, the webapp service will start, mount that volume, and find the secret files ready for it.

The most surprising thing about using Vault with Swarm is that you often don’t need Swarm’s native secret management at all. You can configure Vault to write secrets directly into a shared volume that your application service then reads, bypassing Swarm’s secrets: directive entirely. This gives you a more direct, and often simpler, integration path where Vault is the sole source of truth for secrets, and your application just consumes them from a predictable location.

This pattern allows you to manage secrets with enterprise-grade security and compliance features without overburdening your orchestration layer.

The next natural step is to explore automated secret rotation and dynamic secrets with Vault.

Want structured learning?

Take the full Docker course →