Manage Your Self-Hosted App Secrets With HashiCorp Vault

Manage Your Self-Hosted App Secrets With HashiCorp Vault

Stop scattering API keys in .env files. Set up HashiCorp Vault in Docker to centralize, rotate, and audit every secret your self-hosted apps need.

My .env files were scattered everywhere. One in my Nextcloud directory. Another for my database passwords. A third one for API keys that I swore I’d “organize later.” I had sticky notes with tokens. Passwords in comments. The works.

One day I needed to rotate an API key that was in six different places. I found five of them. The sixth? Still out there somewhere, probably causing issues I don’t even know about yet.

That’s when I realized I needed a proper secrets management system. Not just for production environments, but for my self-hosted homelab too.

Enter HashiCorp Vault.

Why You Actually Need This

Look, I get it. “Just use environment variables” sounds simpler. And for one or two apps, sure. But once you’re running a dozen services, each with their own database credentials, API keys, and tokens, things get messy fast.

Here’s what convinced me:

Centralization. One place to store every secret. Need to update a password? Change it once. Not in twelve .env files.

Audit trails. Vault logs who accessed what and when. That time my database password leaked? I could trace exactly which app grabbed it and when.

Rotation without downtime. Swap credentials while apps keep running. No more “updating passwords at 2 AM” sessions.

Dynamic secrets. Vault can generate database credentials on-demand and auto-expire them. Your app gets a unique username/password that lives for exactly as long as it needs.

Honestly, the initial setup took me an afternoon. The time I’ve saved since? Probably weeks.

What Vault Actually Does

Think of Vault as a fortress for your secrets. But instead of guards, it has encryption. And instead of keys, it has tokens.

You store secrets in Vault. Your apps request them using authentication tokens. Vault checks if that app should have access, then hands over the secret. Every request gets logged.

The beauty is in the abstraction. Your apps never need the actual secrets hardcoded. They just need to know how to ask Vault for them.

Prerequisites

You need:

  • Docker and Docker Compose installed
  • A Linux server (I’m using Ubuntu 22.04, but most distros work)
  • At least 512MB RAM (Vault is surprisingly light)
  • Basic command line comfort

I’m assuming you’ve already got Docker running. If not, grab it first.

Setting Up Vault in Docker

I’ll show you the setup I use. It’s not overkill, but it’s not “dev mode” either. This is for actual production use in a homelab.

Create a directory for Vault:

mkdir -p ~/vault/{config,data,logs}
cd ~/vault

Here’s the docker-compose.yml I landed on after some trial and error:

version: '3.8'

services:
  vault:
    image: hashicorp/vault:latest
    container_name: vault
    restart: unless-stopped
    ports:
      - "8200:8200"
    environment:
      VAULT_ADDR: 'http://0.0.0.0:8200'
      VAULT_API_ADDR: 'http://0.0.0.0:8200'
    cap_add:
      - IPC_LOCK
    volumes:
      - ./config:/vault/config
      - ./data:/vault/data
      - ./logs:/vault/logs
    command: server

The IPC_LOCK capability prevents memory from being swapped to disk. Vault secrets should never touch disk unencrypted.

Now create the Vault config file at config/vault.hcl:

ui = true

listener "tcp" {
  address = "0.0.0.0:8200"
  tls_disable = "true"
}

storage "file" {
  path = "/vault/data"
}

api_addr = "http://127.0.0.1:8200"

About TLS: I’m disabling it here because I run Vault behind a reverse proxy (Nginx Proxy Manager) that handles HTTPS. If you’re exposing Vault directly to the internet, absolutely enable TLS. I’ll show you how later.

Start Vault:

docker-compose up -d

Check the logs to make sure it started:

docker logs vault

You should see something like “Vault server started.” If you see errors about permissions, check that your data directory is writable.

Initializing Vault (This Part Is Critical)

Vault starts sealed. Everything is encrypted. You need to initialize it first.

Set the Vault address in your shell:

export VAULT_ADDR='http://localhost:8200'

Initialize Vault:

docker exec -it vault vault operator init

This outputs five unseal keys and one root token. Save these immediately. I mean it. Write them down. Put them in a password manager. Email them to yourself. Do whatever it takes to not lose these.

You’ll see something like:

Unseal Key 1: xGz7+12abc...
Unseal Key 2: hQw9+34def...
Unseal Key 3: kLm2+56ghi...
Unseal Key 4: pRt6+78jkl...
Unseal Key 5: vYu4+90mno...

Initial Root Token: hvs.abcd1234...

To decrypt Vault, you need three of those five keys (by default). It’s called Shamir’s Secret Sharing. The idea is that no single person has full access.

For a homelab, honestly, I keep all five keys myself. But I store them separately. Three in my password manager. Two in an encrypted note that lives on a different machine.

Unseal Vault with any three keys:

docker exec -it vault vault operator unseal
# Paste first key when prompted
docker exec -it vault vault operator unseal
# Paste second key
docker exec -it vault vault operator unseal
# Paste third key

After the third key, Vault unseals. You can check status:

docker exec -it vault vault status

It should say Sealed: false.

The Unseal Dance (Every Restart)

Here’s the annoying part: Vault seals itself on every restart. By design. It’s a security feature.

So every time your server reboots, you need to manually unseal Vault with three keys.

I wrote a small script for this:

#!/bin/bash
# unseal-vault.sh

export VAULT_ADDR='http://localhost:8200'

echo "Unsealing Vault..."

docker exec -it vault vault operator unseal "$UNSEAL_KEY_1"
docker exec -it vault vault operator unseal "$UNSEAL_KEY_2"
docker exec -it vault vault operator unseal "$UNSEAL_KEY_3"

echo "Vault unsealed."

I store the keys in a separate encrypted file and source them before running this. Not perfect, but better than typing them manually.

Some people use Vault’s auto-unseal feature with cloud KMS. For a homelab, that feels like overkill. I just accept the manual unseal step.

Logging Into Vault

Now authenticate with the root token:

export VAULT_TOKEN='hvs.abcd1234...'
docker exec -it vault vault login
# Paste your root token when prompted

Or use the web UI. Open http://your-server-ip:8200 in a browser. You’ll see the Vault interface. Use your root token to sign in.

The web UI is surprisingly good. I use it for quick checks, but I do most work via CLI.

Creating Your First Secret

Let’s store a database password.

Enable the key-value secrets engine (v2):

docker exec -it vault vault secrets enable -version=2 kv

Store a secret:

docker exec -it vault vault kv put kv/database/postgres username=myapp password=supersecret123

Retrieve it:

docker exec -it vault vault kv get kv/database/postgres

You’ll see:

====== Data ======
Key         Value
---         -----
password    supersecret123
username    myapp

That’s it. You just centralized a secret.

Organizing Secrets Sensibly

I structure mine like this:

kv/
  apps/
    nextcloud/
      db_password
      admin_password
    immich/
      db_password
      api_key
  infrastructure/
    cloudflare/
      api_token
    smtp/
      password
  integrations/
    telegram/
      bot_token
    discord/
      webhook_url

This makes it easy to find things later. Trust me, “flat” organization gets messy fast.

Policies: Who Gets What

The root token has full access. That’s dangerous. You shouldn’t give every app the root token.

Create policies that limit access.

Here’s a policy for an app that only needs Nextcloud database credentials:

docker exec -it vault vault policy write nextcloud-policy - <<EOF
path "kv/data/apps/nextcloud/*" {
  capabilities = ["read"]
}
EOF

This says: “Read access to anything under kv/data/apps/nextcloud/.” That’s it.

Now create a token with that policy:

docker exec -it vault vault token create -policy=nextcloud-policy

You get a token that can only read Nextcloud secrets. Give this to your Nextcloud container. If it gets compromised, the attacker can’t access your other secrets.

AppRole: Better Authentication for Apps

Tokens are simple but not ideal for apps. If a token leaks, it’s valid until it expires.

AppRole is better. It’s like username/password for apps.

Enable AppRole:

docker exec -it vault vault auth enable approle

Create a role for your app:

docker exec -it vault vault write auth/approle/role/nextcloud \
  secret_id_ttl=24h \
  token_ttl=1h \
  token_max_ttl=4h \
  policies="nextcloud-policy"

Get the Role ID:

docker exec -it vault vault read auth/approle/role/nextcloud/role-id

Generate a Secret ID:

docker exec -it vault vault write -f auth/approle/role/nextcloud/secret-id

Your app uses the Role ID and Secret ID to authenticate:

curl --request POST \
  --data '{"role_id":"xxx","secret_id":"yyy"}' \
  http://vault:8200/v1/auth/approle/login

This returns a token. Use that token to fetch secrets.

The Secret ID expires after 24 hours. The token expires after 1 hour. This limits damage if credentials leak.

Connecting an App to Vault

Let’s say you have a Python app that needs a database password.

Install the Vault client:

pip install hvac

Here’s the simplest possible example:

import hvac

client = hvac.Client(url='http://vault:8200', token='your-token-here')

secret = client.secrets.kv.v2.read_secret_version(path='apps/myapp/db')
password = secret['data']['data']['password']

print(f"Database password: {password}")

For production, use AppRole instead of a hardcoded token:

import hvac

client = hvac.Client(url='http://vault:8200')

client.auth.approle.login(
    role_id='xxx',
    secret_id='yyy'
)

secret = client.secrets.kv.v2.read_secret_version(path='apps/myapp/db')
password = secret['data']['data']['password']

Most languages have Vault libraries. Check the official docs for yours.

Dynamic Database Credentials

This is where Vault gets really cool.

Instead of storing static database passwords, Vault can generate them on-demand.

Enable the database secrets engine:

docker exec -it vault vault secrets enable database

Configure it for PostgreSQL:

docker exec -it vault vault write database/config/my-postgres-db \
  plugin_name=postgresql-database-plugin \
  allowed_roles="readonly" \
  connection_url="postgresql://{{username}}:{{password}}@postgres:5432/mydb?sslmode=disable" \
  username="vault" \
  password="vault-password"

Create a role that defines what permissions the generated credentials get:

docker exec -it vault vault write database/roles/readonly \
  db_name=my-postgres-db \
  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"

Now when an app needs database access:

docker exec -it vault vault read database/creds/readonly

Vault generates a unique username and password, creates the user in PostgreSQL, and returns the credentials. After 1 hour, Vault deletes that user automatically.

Your app gets a credential that lives exactly as long as it needs. No more shared passwords.

I use this for apps that need read-only database access. If one app gets compromised, the attacker’s credentials expire quickly, and they can’t affect other apps.

Secret Rotation

Let’s say you need to change a password. Maybe it leaked. Maybe it’s just time.

Update the secret:

docker exec -it vault vault kv put kv/apps/myapp/db password=newpassword456

Vault keeps old versions. Check version history:

docker exec -it vault vault kv get -version=1 kv/apps/myapp/db

This saved me once. I accidentally overwrote a secret and couldn’t remember the old value. Vault remembered.

For proper rotation, update the secret in Vault first, then restart your apps so they fetch the new value.

Some apps support hot-reloading secrets. Most don’t. Plan for a brief restart.

Backing Up Vault

Vault’s data lives in ./data. Back it up.

I use a simple cronjob:

#!/bin/bash
# backup-vault.sh

DATE=$(date +%Y%m%d)
tar -czf ~/backups/vault-$DATE.tar.gz ~/vault/data
find ~/backups/vault-*.tar.gz -mtime +30 -delete

Run this daily. Keep 30 days of backups.

Also back up your unseal keys and root token separately. Preferably offline. USB drive, encrypted file, whatever. Just don’t lose them.

If you lose the unseal keys, your Vault is bricked. The data is encrypted and there’s no recovery. I’m serious about this.

Enabling HTTPS (If You Need It)

If you’re accessing Vault from outside your network, enable TLS.

Generate a self-signed cert (or use Let’s Encrypt):

openssl req -x509 -newkey rsa:4096 -keyout vault-key.pem -out vault-cert.pem -days 365 -nodes

Move the cert and key to ./config/.

Update vault.hcl:

listener "tcp" {
  address = "0.0.0.0:8200"
  tls_disable = "false"
  tls_cert_file = "/vault/config/vault-cert.pem"
  tls_key_file = "/vault/config/vault-key.pem"
}

Restart Vault:

docker-compose restart

Now access Vault via https://your-server-ip:8200.

I run Vault behind Nginx Proxy Manager, so I let NPM handle TLS and keep Vault’s listener plain. Either approach works.

Monitoring and Auditing

Vault logs every access attempt. Enable the audit log:

docker exec -it vault vault audit enable file file_path=/vault/logs/audit.log

Now every secret access gets logged to ./logs/audit.log.

I grep this occasionally to see what’s being accessed:

grep "kv/data/apps/myapp" ~/vault/logs/audit.log

You can also send logs to syslog, a SIEM, or whatever you use for log aggregation.

Common Mistakes I Made

Losing unseal keys. I did this once in a test environment. Had to nuke everything and start over. Don’t be me.

Using root token everywhere. I got lazy early on and gave apps the root token. Bad idea. When one app had a bug that logged secrets, the root token showed up. Use policies.

Not backing up. Vault crashed once (hardware issue). My backups were two weeks old. I lost secrets added in between. Back up daily.

Forgetting to unseal after reboot. My server auto-restarts at 4 AM for updates. I woke up to apps failing because Vault was sealed. Now I have a notification that reminds me to unseal after reboots.

Overly complex policies. I started with super granular policies. “This app can only read this one specific key.” It was a nightmare to maintain. Now I use broader policies per app and trust my network segmentation.

Vault vs. Alternatives

Bitwarden/Vaultwarden: Great for personal passwords. Not designed for app secrets. No API policies, no dynamic secrets, no audit logs.

Doppler: Cloud-based secrets manager. Solid, but you’re trusting a third party. I wanted everything self-hosted.

Environment variables in Docker Compose: Simple, but you end up with secrets in plaintext files. No rotation, no audit trail.

KeePass files: I tried this. Mounting a KeePass database and parsing it from apps. It worked but felt janky. Vault is purpose-built for this.

Vault is overkill if you have two apps and three secrets. For anything beyond that, it’s worth it.

Integrating with Docker Compose

Here’s how I connect an app to Vault:

services:
  myapp:
    image: myapp:latest
    environment:
      VAULT_ADDR: http://vault:8200
      VAULT_TOKEN: ${VAULT_TOKEN}
    depends_on:
      - vault

Then in the app’s startup script:

#!/bin/bash
export DB_PASSWORD=$(vault kv get -field=password kv/apps/myapp/db)
exec python app.py

The app fetches the secret at startup. Not perfect (secrets still end up in environment variables), but better than hardcoding.

For more security, use the Vault agent. It handles authentication and injects secrets into files that your app reads.

Performance Considerations

Vault adds latency. Every secret fetch is a network request.

For most homelab use, this is negligible. I measured about 10-20ms per request on my local network.

If you’re fetching secrets on every HTTP request (don’t do this), cache them in memory. Fetch once at startup, refresh hourly.

Vault can handle thousands of requests per second. My homelab hits maybe ten per minute. Performance is not a concern for self-hosters.

When Vault Breaks

Vault won’t start: Check docker logs vault. Usually it’s a config syntax error or permissions issue on the data directory.

Can’t unseal: Make sure you’re using three different keys. I once pasted the same key three times and wondered why it didn’t work.

Apps can’t reach Vault: Check Docker networks. Vault needs to be on the same network as your apps, or exposed via host network mode.

403 errors: Your token doesn’t have permission. Check policies. Use vault token lookup to see what policies are attached to a token.

Lost root token: You can generate a new one using the unseal keys. Google “vault generate root token” for steps. It’s a pain but possible.

Things I Still Want to Improve

Auto-unseal: I’d like Vault to unseal automatically on reboot. This requires a secondary secrets manager (AWS KMS, another Vault instance, etc.). Chicken-and-egg problem. Haven’t solved it yet.

Secret injection: Instead of apps fetching secrets, I want Vault agent to inject them into files. It’s on my to-do list.

Better monitoring: Right now I just check logs manually. I should set up alerts for failed authentication attempts.

LDAP integration: My homelab doesn’t have LDAP, but if it did, I’d use it for Vault authentication instead of tokens.

Is It Worth It?

For me? Absolutely.

The initial setup took an afternoon. Since then, I’ve rotated database passwords with zero downtime, audited which apps accessed what secrets, and caught a misconfigured service that was hammering Vault (oops).

If you run more than a handful of self-hosted apps, Vault pays for itself in saved time and better security.

That said, it’s not for everyone. If you’re happy with .env files and they work for you, stick with them. I’m not here to preach.

But if you’ve ever lost track of which password is where, or spent an hour updating the same secret in ten places, give Vault a shot.

FAQ

Do I need to run Vault in Docker? No. You can install it directly on your server. I prefer Docker because it’s isolated and easy to back up.

What if I lose the unseal keys? You’re screwed. There’s no recovery. The data is encrypted with those keys. Back them up. Seriously.

Can I use Vault for personal passwords? You can, but it’s not designed for that. Use Bitwarden or Vaultwarden instead.

Does Vault work on a Raspberry Pi? Yes. I tested it on a Pi 4. It’s a bit slow, but functional. Use the ARM Docker image.

How much storage does Vault need? My Vault instance uses about 50MB for hundreds of secrets. Storage is not a concern.

Can multiple apps use the same secret? Yes. Store the secret once, create policies that allow multiple apps to read it.

Is Vault overkill for a homelab? Maybe. Depends on your setup. Two apps? Probably overkill. Twenty apps? Definitely worth it.

Does Vault support SQLite? For storage? No. It uses file-based storage (like I showed), Consul, or cloud backends. For dynamic secrets? No SQLite support. PostgreSQL, MySQL, MongoDB, and others are supported.

Can I run Vault in high availability? Yes, with Consul as the backend. Overkill for a homelab, but possible.

What happens if Vault crashes? Apps can’t fetch secrets until it’s back up. Cache secrets in memory so apps survive short outages.

Can I import my existing secrets? Yes. Write a script that reads your .env files and pushes secrets to Vault via the API. I did this with a Python script. Took 20 minutes.

How do I revoke access? Revoke tokens with vault token revoke. For AppRole, delete the Secret ID. Existing tokens remain valid until they expire.

Does Vault support 2FA? Not directly. But you can integrate with LDAP or SSO providers that enforce 2FA.

Can I use Vault with Kubernetes? Yes. Vault has native Kubernetes integration. Out of scope for this guide, but the docs are good.

Is Vault free? The open-source version is free and fully functional. HashiCorp offers an enterprise version with support and extra features. I use the free version.

How often should I rotate secrets? Depends. I rotate critical secrets (database root passwords) every six months. API keys when I suspect compromise. App-specific credentials (dynamic secrets) rotate automatically.

Can I run Vault without Docker? Yes. Download the binary, create a systemd service, point it at a config file. Same concept, different packaging.

What if my server runs out of disk space? Vault stores data in ./data. Keep an eye on disk usage. My instance uses minimal space, but audit logs can grow large. Rotate them.

Does Vault work with Windows? Yes, but I’ve never tried it. The Docker setup should work on Docker Desktop for Windows. Native installation is also possible.


That’s Vault. It’s not magic. It’s just a really well-designed tool for a problem most self-hosters eventually face.

Set it up once, use it forever.

Stay in the loop 📬

Get self-hosting tutorials, tool reviews, and infrastructure tips delivered to your inbox. No spam, unsubscribe anytime.

Join 0 self-hosters. Free forever.