Using Vault KV V2 Secrets with Docker Compose

This tutorial explains how to securely consume secrets stored in HashiCorp Vault Key-Value (KV) version 2 engine within a Docker Compose environment.

As Vault operators

Before the application can consume secrets, the Vault team must prepare the secret engine, the secret itself, and the access permissions.

Store a kvv2 secret

Assuming the KV V2 engine is enabled at the path secret/, use the following command to store a secret:

vault kv put secret/my-application/config \
    api_key="secret-api-token-value" \
    db_password="very-secure-password"

To verify the secret was created:

vault kv get secret/my-application/config

Create a read-only policy

Create a policy file named my-app-policy.hcl that grants read access specifically to this secret path. Note that for KV V2, the policy must target the data/ subpath.

# Access for KV V2
path "secret/data/my-application/config" {
  capabilities = ["read"]
}

Write the policy to Vault:

vault policy write my-app-read-policy my-app-policy.hcl

Generate a service token

Generate a token for the application to use. It is recommended to use a periodic token or a token with a TTL that fits your deployment cycle.

vault token create -policy="my-app-read-policy" -period="24h" -display-name="docker-compose-app"

Important: Copy the token value (e.g., hvs.CAES...) and provide it securely to the application team.


As Apps operators

Once you have the Vault address and a token, you can configure your Docker Compose setup to retrieve secrets.

Option 1: Pre-fetching via shell script

Most applications expect secrets to be present in environment variables or files at startup. You can use a wrapper script to fetch secrets from Vault before starting your main process.

Entrypoint script

#!/bin/sh
set -e

# Fetch secrets from Vault using curl and jq
# Note: KV V2 returns data nested under .data.data
VAULT_RESPONSE=$(curl -s -H "X-Vault-Token: $VAULT_TOKEN" "$VAULT_ADDR/v1/secret/data/my-application/config")

# Export values as environment variables
export API_KEY=$(echo $VAULT_RESPONSE | jq -r '.data.data.api_key')
export DB_PASSWORD=$(echo $VAULT_RESPONSE | jq -r '.data.data.db_password')

# Execute the main command
exec "$@"

Ensure the script is executable: chmod +x entrypoint.sh.

Docker compose config

Update your docker-compose.yml to use this script. You’ll need curl and jq installed in your image.

services:
  my-app:
    image: my-app-with-tools:latest
    volumes:
      - ./entrypoint.sh:/entrypoint.sh
    entrypoint: ["/entrypoint.sh"]
    command: ["npm", "start"]
    environment:
      - VAULT_ADDR=https://vault.example.com:8200
      - VAULT_TOKEN=${VAULT_TOKEN}

This approach ensures that your application never “sees” the Vault token directly if it doesn’t need to, and it receives the actual secrets it needs to function.

Option 2: Pre-fetching via systemd service definition

In production environments where your Docker Compose project is managed by systemd, you can use a separate service to pre-fetch secrets and store them in an environment file. This separates secret retrieval from the application container’s lifecycle.

Secret fetcher script

Create a script /usr/local/bin/fetch-app-secrets.sh:

#!/bin/bash
set -e

OUTPUT_DIR="/opt/my-app"
OUTPUT_ENV_FILE="${OUTPUT_DIR}/.env.secrets"

VAULT_ADDR="https://vault.example.com:8200"
# Token should be managed securely, e.g., via a protected file
VAULT_TOKEN=$(cat /etc/vault/app-token)
SECRET_PATH="/v1/secret/data/my-application/config"

VAULT_RESPONSE=$(curl -Ls -H "X-Vault-Token: $VAULT_TOKEN" "${VAULT_ADDR}${SECRET_PATH}")

# Generate an env file for Docker Compose
echo "API_KEY=$(echo $VAULT_RESPONSE | jq -r '.data.data.api_key')" > ${OUTPUT_ENV_FILE}
echo "DB_PASSWORD=$(echo $VAULT_RESPONSE | jq -r '.data.data.db_password')" >> ${OUTPUT_ENV_FILE}

chmod 600 ${OUTPUT_ENV_FILE}

Systemd service configuration

Create a systemd unit file /etc/systemd/system/my-app-secrets.service:

[Unit]
Description=Fetch secrets from Vault for my-app
Before=my-app-docker.service

[Service]
Type=oneshot
ExecStart=/usr/local/bin/fetch-app-secrets.sh
User=root
Group=root

[Install]
WantedBy=multi-user.target

Then, configure your main application service (e.g., my-app-docker.service) to depend on it:

[Unit]
Description=My App Docker Compose Service
Requires=my-app-secrets.service
After=my-app-secrets.service

[Service]
WorkingDirectory=/opt/my-app
ExecStart=/usr/bin/docker compose up
ExecStop=/usr/bin/docker compose down

[Install]
WantedBy=multi-user.target

Docker Compose Config

In your docker-compose.yml, reference the generated environment file:

services:
  my-app:
    image: my-app:latest
    env_file:
      - .env.secrets

This method is robust as it ensures secrets are fresh every time the service starts, without requiring Vault tools inside the application container.