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.