Self-Hosting HashiCorp Vault with Docker Compose
What Is HashiCorp Vault?
HashiCorp Vault is the most widely deployed secrets management platform in the industry. It stores secrets, generates dynamic credentials for databases and cloud providers, handles encryption as a service, and issues TLS certificates through its built-in PKI engine. If you manage more than a handful of API keys or database passwords, Vault replaces scattered .env files and shared spreadsheets with a centralized, audited, access-controlled system.
Vault is licensed under the BSL 1.1 (Business Source License) since August 2023. All core features — secret engines, dynamic credentials, transit encryption, PKI — remain available. The community fork OpenBao exists under MPL-2.0 if the BSL concerns you.
Prerequisites
- A Linux server (Ubuntu 22.04+ recommended)
- Docker and Docker Compose installed (guide)
- 1 GB of free RAM (512 MB minimum for dev mode)
- 2+ CPU cores recommended for production
- A domain name (recommended for TLS)
Docker Compose Configuration
Vault supports multiple storage backends. Raft integrated storage is recommended for self-hosting — it’s built-in, requires no external dependencies, and handles leader election automatically.
Production Setup (Raft Storage)
Create a directory for Vault:
mkdir -p vault && cd vault
Create config.hcl:
# Vault server configuration
storage "raft" {
path = "/vault/data"
node_id = "vault-1"
}
listener "tcp" {
address = "0.0.0.0:8200"
tls_disable = 1 # Set to 0 and configure TLS for production
}
api_addr = "http://vault:8200"
cluster_addr = "http://vault:8201"
ui = true
# Disable memory locking if running in Docker without IPC_LOCK
disable_mlock = false
Create docker-compose.yml:
services:
vault:
image: hashicorp/vault:1.21.4
container_name: vault
ports:
- "8200:8200" # API and web UI
- "8201:8201" # Cluster communication (Raft)
volumes:
- vault-data:/vault/data
- ./config.hcl:/vault/config/config.hcl:ro
cap_add:
- IPC_LOCK # Required for memory locking (mlock)
environment:
VAULT_ADDR: "http://127.0.0.1:8200"
command: vault server -config=/vault/config/config.hcl
healthcheck:
test: ["CMD", "vault", "status"]
interval: 30s
timeout: 10s
retries: 3
start_period: 30s
restart: unless-stopped
volumes:
vault-data:
Start the stack:
docker compose up -d
The IPC_LOCK capability is critical — Vault uses mlock to prevent sensitive data from being swapped to disk. Without it, secrets could leak to swap partitions.
Development Setup (Quick Start)
For testing and learning, dev mode runs entirely in memory:
services:
vault:
image: hashicorp/vault:1.21.4
container_name: vault-dev
ports:
- "8200:8200"
environment:
VAULT_DEV_ROOT_TOKEN_ID: dev-root-token # Change this
VAULT_DEV_LISTEN_ADDRESS: "0.0.0.0:8200"
cap_add:
- IPC_LOCK
command: vault server -dev
restart: unless-stopped
Dev mode is pre-initialized, auto-unsealed, and has an in-memory backend. All data is lost on restart. Never use this for real secrets.
Initial Setup
Vault requires a one-time initialization that generates unseal keys and a root token.
Initialize Vault
docker exec -it vault vault operator init
This outputs 5 unseal keys and 1 root token. Save these immediately — losing the unseal keys means losing access to all secrets. Store them in separate secure locations (password manager, physical safe, trusted colleagues).
Unseal Vault
Vault starts sealed — it knows encrypted data exists but can’t read it. You need 3 of the 5 unseal keys (the default threshold) to unseal:
docker exec -it vault vault operator unseal <key-1>
docker exec -it vault vault operator unseal <key-2>
docker exec -it vault vault operator unseal <key-3>
After three valid keys, Vault transitions to unsealed state and can serve requests.
Login
docker exec -it vault vault login <root-token>
Then access the web UI at http://your-server:8200. Log in with the root token.
Create a non-root admin policy and token immediately. The root token should only be used for initial setup and break-glass scenarios.
Key Features
Secret Engines
Vault organizes secrets into engines, each with different capabilities:
| Engine | Purpose | Example |
|---|---|---|
| KV v2 | Static secrets with versioning | API keys, passwords |
| Database | Dynamic credentials | Temporary PostgreSQL users |
| Transit | Encryption as a service | Encrypt data without managing keys |
| PKI | Certificate authority | Issue TLS certificates |
| SSH | SSH certificate signing | Short-lived SSH access |
| AWS/GCP/Azure | Cloud IAM credentials | Temporary cloud access keys |
Dynamic Secrets
Instead of storing a database password, Vault generates a temporary PostgreSQL user with a 1-hour TTL:
# Enable database engine
vault secrets enable database
# Configure PostgreSQL connection
vault write database/config/myapp \
plugin_name=postgresql-database-plugin \
allowed_roles="readonly" \
connection_url="postgresql://{{username}}:{{password}}@db:5432/myapp" \
username="vault_admin" \
password="vault_admin_password"
# Create a role that generates temporary users
vault write database/roles/readonly \
db_name=myapp \
creation_statements="CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}'; GRANT SELECT ON ALL TABLES IN SCHEMA public TO \"{{name}}\";" \
default_ttl="1h" \
max_ttl="24h"
# Generate a credential
vault read database/creds/readonly
Each credential is unique, time-limited, and automatically revoked when it expires. If compromised, you revoke just that credential — not a shared password used everywhere.
Transit Encryption
Encrypt application data without your app handling key management:
vault secrets enable transit
vault write -f transit/keys/my-app-key
# Encrypt
vault write transit/encrypt/my-app-key plaintext=$(echo "sensitive data" | base64)
# Decrypt
vault write transit/decrypt/my-app-key ciphertext="vault:v1:..."
Your application never sees the encryption key. Vault handles key rotation, versioning, and access control.
Configuration
Auto-Unseal
Manual unsealing is impractical for automated deployments. Vault supports auto-unseal using external key management:
| Method | Service | Notes |
|---|---|---|
| Transit | Another Vault instance | Use one Vault to unseal another |
| AWS KMS | Amazon KMS | Most popular cloud option |
| Azure Key Vault | Microsoft Azure | For Azure environments |
| GCP Cloud KMS | Google Cloud | For GCP environments |
For self-hosted setups, Transit auto-unseal with a second Vault instance is the most practical option. Alternatively, store unseal keys in a password manager and script the unseal process.
Audit Logging
Enable audit logging to track every operation:
vault audit enable file file_path=/vault/logs/audit.log
Every secret read, write, and token creation is logged with timestamps, client identity, and request details. This is essential for compliance and incident response.
Authentication Methods
| Method | Use Case |
|---|---|
| Token | Default, programmatic access |
| AppRole | CI/CD pipelines, services |
| LDAP | Active Directory integration |
| OIDC | SSO with Authentik, Keycloak |
| Kubernetes | Pod-level secret injection |
| Userpass | Simple username/password |
Reverse Proxy
Place Vault behind a reverse proxy for TLS termination:
# Nginx configuration
server {
listen 443 ssl;
server_name vault.example.com;
location / {
proxy_pass http://vault:8200;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
See Reverse Proxy Setup for complete configuration with Nginx Proxy Manager, Traefik, or Caddy.
Backup
Vault’s Raft storage supports integrated snapshots:
# Create a snapshot
docker exec -it vault vault operator raft snapshot save /vault/data/backup.snap
# Copy to host
docker cp vault:/vault/data/backup.snap ./vault-backup-$(date +%Y%m%d).snap
Schedule daily snapshots via cron. Test restores periodically — an untested backup is not a backup.
See Backup Strategy for a complete 3-2-1 backup approach.
Troubleshooting
Vault is sealed after restart
Symptom: After a Docker restart, Vault shows “sealed” and rejects all requests.
Fix: Vault seals itself on restart by default. You must unseal it again with 3 of 5 unseal keys. For automated environments, configure auto-unseal (Transit, AWS KMS, etc.).
Permission denied errors
Symptom: permission denied on secret operations despite being authenticated.
Fix: Check your token’s policies. The default policy grants minimal access. Create policies that match your access patterns:
vault policy write my-app - <<EOF
path "secret/data/my-app/*" {
capabilities = ["create", "read", "update", "delete", "list"]
}
EOF
Container exits with “mlock not supported”
Symptom: Vault container exits immediately with mlock errors.
Fix: Add cap_add: [IPC_LOCK] to your Docker Compose, or set disable_mlock = true in config.hcl (less secure — allows secrets to be swapped to disk).
Resource Requirements
- RAM: 512 MB idle, 1-2 GB under load with multiple secret engines
- CPU: Low for typical usage; password hashing and PKI certificate generation spike CPU
- Disk: 1 GB for Vault itself, grows with Raft storage and audit logs
Verdict
HashiCorp Vault is the gold standard for secrets management. Nothing else matches its breadth — dynamic secrets, PKI, transit encryption, and dozens of auth methods. The operational overhead of initialization and unsealing is the tradeoff for that security model.
For homelabs managing 10+ services, Vault replaces scattered .env files with centralized, audited secret access. The learning curve is steep but the payoff is real.
For simpler needs — just storing and sharing secrets with a web UI — Infisical is easier to set up and has a friendlier interface. If BSL licensing is a concern, OpenBao is a fully open-source Vault fork under MPL-2.0.
Related
Get self-hosting tips in your inbox
Get the Docker Compose configs, hardware picks, and setup shortcuts we don't put in articles. Weekly. No spam.
Comments