HashiCorp Vault with Docker Compose: Persistent Storage and Dynamic Secrets (Part 2)

Miroslav MilakMiroslav Milak
6 min read

In Part 1, you set up a simple HashiCorp Vault instance using Docker Compose in development mode - perfect for local testing and quick prototyping. It stores everything in memory, which is ideal for developers who want to explore Vault features fast without setup overhead.

⚠️ In dev mode, all secrets are lost when the container stops.

In this second part of the series, we’ll take a small step toward a more realistic setup:

  • Enable persistent storage so secrets survive restarts,

  • Use manual unsealing (a core Vault security feature),

  • Connect Vault to a MySQL database to issue dynamic secrets - temporary credentials that expire automatically.

This guide is still beginner-friendly and assumes no prior production Vault experience. You’ll build confidence by running everything locally with Docker, just like in Part 1, but with added layers of realism.


🧠 Why dynamic secrets?

Unlike static secrets, dynamic secrets are generated on demand. When an app or user needs access to a database, Vault can create a temporary username and password with limited permissions that self-destruct after an hour or so. This removes the need to manage and rotate credentials manually.


🔁 Quick Recap of Part 1

Previously, we:

  • Ran Vault in development mode using Docker Compose.

  • Stored secrets in memory (they were lost after each restart).

  • Explored the UI and created static secrets manually.

Now, we're building a persistent, more production-like setup with real secret automation


Prerequisites

Before starting, ensure you have:

🪟 Windows users: Use set instead of export for environment variables.


Setting Up Vault with Persistent Storage

Step 1: Create the Docker Compose File

Create a file named docker-compose.yml in a new directory (e.g., vault-docker):

services:
  vault:
    image: hashicorp/vault:latest
    ports:
      - "8200:8200"
    environment:
      - VAULT_ADDR=http://0.0.0.0:8200
    cap_add:
      - IPC_LOCK
    volumes:
      - vault-data:/vault/data
      - ./vault-config:/vault/config
    command: vault server -config=/vault/config/vault.hcl
    depends_on:
      - mysql
  mysql:
    image: mysql:8
    container_name: mysql
    environment:
      MYSQL_ROOT_PASSWORD: rootpass
    ports:
      - "3306:3306"

volumes:
  vault-data:

Key Notes:

  • vault-data: A Docker volume for persistent storage, keeping secrets between restarts.

  • vault-config: Maps a local directory for the Vault configuration file.

  • mysql: A MySQL container for dynamic secrets, with a root password (rootpass).

  • depends_on: Ensures MySQL starts before Vault for dynamic secrets setup.

Step 2: Create the Vault Configuration

Unlike dev mode, we now provide an actual configuration file (vault.hcl) that Vault reads when starting in server mode.

Create a vault-config directory in vault-docker and add a file named vault.hcl.

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

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

ui = true

Key Notes:

  • storage "file": Stores secrets in /vault/data, mapped to the vault-data volume.

  • tls_disable = 1: Disables TLS for simplicity; in production, enable TLS with certificates.

  • ui = true: Enables the Vault web UI.

Step 3: Start and Initialize Vault

  1. Navigate to vault-docker folder and start the containers:

     docker-compose up -d
    
  2. Verify the containers are running:

     docker ps
    

    You should see vault and mysql containers.

  3. Initialize Vault (run once):

     export VAULT_ADDR=http://localhost:8200
     vault operator init -key-shares=1 -key-threshold=1
    

    This outputs one unseal key and a root token. Save them securely (e.g., in a password manager). Normally, Vault generates multiple keys (e.g., five, with a threshold of three) for added security, but we’re using a single key for simplicity. Do not use a single key in production, as it reduces security.

  4. Unseal Vault:

     vault operator unseal <unseal-key>
    

    Use the single unseal key from the init output. Vault is now unsealed and ready.

  5. Log in with the root token:

     export VAULT_TOKEN=<root-token>
     vault login
    

Step 4: Set Up MySQL Dynamic Secrets

Vault can generate temporary MySQL credentials that expire after a set time (e.g., 1 hour). Let’s configure this:

# Enable the database secrets engine
vault secrets enable database

# Configure MySQL connection so Vault can communicate with MySQL
vault write database/config/my-mysql \
    plugin_name=mysql-database-plugin \
    connection_url="root:rootpass@tcp(mysql:3306)/" \
    allowed_roles="my-role"

# Create a role that controls how dynamic users are created
vault write database/roles/my-role \
    db_name=my-mysql \
    creation_statements="CREATE USER '{{name}}'@'%' IDENTIFIED BY '{{password}}'; GRANT SELECT ON *.* TO '{{name}}'@'%';" \
    default_ttl="1h" \
    max_ttl="24h"

# Test: generate credentials on the fly!
vault read database/creds/my-role

Key Notes:

  • This creates temporary MySQL credentials that expire after 1 hour (default_ttl="1h").

  • The credentials grant SELECT permissions on all databases.

  • Run vault read database/creds/my-role again to generate new credentials.

Step 5: Access Vault

  • Web UI: Open http://localhost:8200 (or http://127.0.0.1:8200 if localhost is blocked). Log in with the root token. Navigate to “Secrets” to view or create secrets.

  • CLI: Check Vault status or store a secret:

      vault status
      vault kv put secret/my-app db_password=supersecret
      vault kv get secret/my-app
    
  • Secrets persist across container restarts due to the vault-data volume.

Step 6: Stop Vault

To stop the containers:

docker-compose down

On restart, you’ll need to unseal Vault again using the single unseal key.


Best Practices

  • Secure Unseal Key and Root Token: Store the unseal key and root token in a secure location (e.g., password manager or HSM). Never store them in plain text or version control. In production, use multiple keys (e.g., three out of five) for better security.

  • Use Multiple Unseal Keys: In production, use e.g., 3-of-5 shares.

  • Limit Root Token Use: Use the root token only for setup. Create non-root users (see Part 1 experiments) for regular access.

  • Backup Storage: The vault-data volume persists data, but back up the storage directory regularly in production.

  • Enable TLS: In production, set tls_disable = 0 in vault.hcl and provide TLS certificates.

  • Dynamic Secrets Are Short-Lived: Vault-created DB credentials only live for the TTL you set. This reduces risk if credentials leak or are forgotten.


Troubleshooting

  • Vault Not Accessible: Ensure containers are running (docker ps) and port 8200 is free (lsof -i :8200). Check firewall settings.

  • Unseal Fails: Verify the unseal key is correct. If lost, re-run vault operator init -key-shares=1 -key-threshold=1 (this resets Vault).

  • MySQL Connection Error: Confirm MySQL is running (docker logs mysql) and the connection_url matches the MySQL container’s credentials.


What’s Next?

You’ve set up Vault with persistent storage and dynamic MySQL credentials! This setup is closer to production but still runs on a single node. In Part 3, we’ll create a three-node Vault cluster for high availability. Try these experiments:

  • Store a secret in the web UI and retrieve it via CLI.

  • Generate new MySQL credentials and test them with a MySQL client.

  • Explore token time-to-live (TTL) behavior for dynamic secrets.

Share your progress in the comments or join the HashiCorp Community Forum!


Resources

Note: For questions or setup help, reach out, comment below or check the Vault documentation.

0
Subscribe to my newsletter

Read articles from Miroslav Milak directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Miroslav Milak
Miroslav Milak

HashiCorp Vault SME Certified | Passionate about secure secrets management and cloud infrastructure. Sharing insights, tutorials, and best practices on HashiNode to help engineers build resilient, scalable systems. Advocate for DevSecOps and cutting-edge security solutions.