DEV Community

Cover image for Securing 15-Package Monorepo in Docker: Production Secrets for Self-Hosted AI Wallets
Wallet Guy
Wallet Guy

Posted on

Securing 15-Package Monorepo in Docker: Production Secrets for Self-Hosted AI Wallets

Securing a 15-Package Monorepo in Docker: Production Secrets for Self-Hosted AI Wallets

Self-hosting your AI agent's wallet infrastructure in Docker means you control the private keys, the server, and the rules — but only if you handle production secrets correctly. Most guides stop at docker compose up -d and leave you wondering how to get your master password off a sticky note and into something production-safe. This post walks through exactly that: how WAIaaS, a self-hosted Wallet-as-a-Service for AI agents, handles secrets in its 15-package monorepo deployment so your keys never leave your server.

Why This Actually Matters

Hosted wallet services are convenient. You sign up, get an API key, and your AI agent starts moving funds. But you've just handed custody of your agent's private keys to a third party. Their uptime is your uptime. Their security practices are your security practices. Their terms of service are your terms of service.

Running your own wallet infrastructure is the crypto equivalent of running your own email server — except WAIaaS makes it genuinely practical rather than a weekend of pain. When your AI agent's wallet lives on your hardware, behind your firewall, authenticated with credentials only you hold, the threat model changes completely. There's no hosted API to breach, no shared infrastructure to worry about, and no rate limits imposed by someone else's pricing tier. Your keys, your server, your rules.

The catch? You have to actually secure it. And in a monorepo with 15 packages running inside Docker, "secure it" means more than setting a strong password.

Understanding WAIaaS's Three-Layer Security Model

Before touching a single config file, it helps to understand what you're actually protecting.

WAIaaS uses three distinct authentication layers:

  • masterAuth — System administrator level. Uses Argon2id hashing. Creates wallets, manages sessions, configures policies. This is the credential you most need to protect.
  • ownerAuth — Fund owner level. Uses SIWS/SIWE (Sign-In With Solana / Sign-In With Ethereum). Required for transaction approval and kill-switch recovery.
  • sessionAuth — AI agent level. JWT HS256 tokens. What your agent actually uses for day-to-day operations like checking balances and sending transactions.

The master password is the root of this tree. If it leaks, everything downstream is compromised. Docker Secrets is how you stop it from leaking.

The Problem With Environment Variables

The naive approach to secrets in Docker looks like this:

# DON'T do this in production
docker run -e WAIAAS_MASTER_PASSWORD=my-secret-password waiaas/daemon:latest
Enter fullscreen mode Exit fullscreen mode

Environment variables in Docker have a few uncomfortable properties. They show up in docker inspect. They appear in process listings on some systems. They get logged if you're not careful. They end up in shell history. And they're visible to any process inside the container.

WAIaaS's Docker entrypoint supports Docker Secrets specifically to give you a better path. The docker-compose.secrets.yml overlay exists for exactly this production scenario — separating secret injection from your base compose configuration.

Setting Up Production Secrets

Here's the practical path from "it works on my laptop" to "I'd actually run this in production."

Step 1: Create Your Secret Files

# Create a secrets directory — keep this out of version control
mkdir -p secrets
echo "your-very-secure-master-password" > secrets/master_password.txt
chmod 600 secrets/master_password.txt
Enter fullscreen mode Exit fullscreen mode

The chmod 600 is non-negotiable. Docker Secrets are mounted as files inside the container, and you want those files readable only by root.

Add secrets/ to your .gitignore immediately. This is the kind of mistake that's very hard to undo once it's in git history.

Step 2: Deploy With the Secrets Overlay

WAIaaS ships a docker-compose.secrets.yml overlay specifically for this:

docker compose -f docker-compose.yml -f docker-compose.secrets.yml up -d
Enter fullscreen mode Exit fullscreen mode

The overlay pattern means your base docker-compose.yml stays clean and committable to version control, while the secrets overlay stays off of it. Different environments (staging, production) can have different overlays without touching the base configuration.

Step 3: Verify the Deployment

# Check the daemon is healthy
docker compose logs -f

# The health endpoint is what keeps the container marked healthy
curl http://127.0.0.1:3100/health
Enter fullscreen mode Exit fullscreen mode

The compose file includes a built-in healthcheck that hits http://localhost:3100/health every 30 seconds, with a 5-second timeout and 10-second start period. If your container shows as unhealthy, the logs will tell you why before you start chasing ghosts.

The Auto-Provision Path (For First-Time Setup)

If you're starting fresh and don't want to pre-generate a master password, WAIaaS supports auto-provisioning:

docker run -d \
  --name waiaas \
  -p 127.0.0.1:3100:3100 \
  -v waiaas-data:/data \
  -e WAIAAS_AUTO_PROVISION=true \
  ghcr.io/waiaas/waiaas:latest
Enter fullscreen mode Exit fullscreen mode

With WAIAAS_AUTO_PROVISION=true, the entrypoint generates a random master password on first start and writes it to /data/recovery.key. Retrieve it before doing anything else:

docker exec waiaas cat /data/recovery.key
Enter fullscreen mode Exit fullscreen mode

Store that somewhere safe — a password manager, an encrypted file, somewhere that isn't a sticky note. Then once you're set up, you can harden it with the CLI:

waiaas set-master
Enter fullscreen mode Exit fullscreen mode

The auto-provision path is great for getting started, but treat the recovery key like a root password, because it is one.

What the Full Compose Configuration Looks Like

Here's the production-ready base configuration:

services:
  daemon:
    image: ghcr.io/waiaas/waiaas:latest
    container_name: waiaas-daemon
    ports:
      - "127.0.0.1:3100:3100"
    volumes:
      - waiaas-data:/data
    environment:
      - WAIAAS_DATA_DIR=/data
      - WAIAAS_DAEMON_HOSTNAME=0.0.0.0
    env_file:
      - path: .env
        required: false
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3100/health"]
      interval: 30s
      timeout: 5s
      start_period: 10s
      retries: 3

volumes:
  waiaas-data:
    driver: local
Enter fullscreen mode Exit fullscreen mode

A few things worth noting here:

Port binding to 127.0.0.1: The default port binding is 127.0.0.1:3100:3100. This is intentional — it means the API is only accessible locally, not from other machines on your network. If you're running this on a VPS and need remote access, put a reverse proxy with TLS in front of it rather than binding to 0.0.0.0.

Named volume for data: Wallet data lives in a named Docker volume. Running docker compose down preserves your data. You have to explicitly run docker compose down -v to delete it. This is the right default for anything holding cryptographic keys.

Non-root user: The WAIaaS Docker image runs as UID 1001. It's not running as root inside the container.

The Key Environment Variables

Most of what you need to configure lives in environment variables rather than config files:

WAIAAS_AUTO_PROVISION=true              # Auto-generate master password on first start
WAIAAS_DAEMON_PORT=3100                 # Listening port
WAIAAS_DAEMON_HOSTNAME=0.0.0.0         # Bind address
WAIAAS_DAEMON_LOG_LEVEL=info            # Log level (trace/debug/info/warn/error)
WAIAAS_DATA_DIR=/data                   # Data directory
WAIAAS_RPC_SOLANA_MAINNET=<url>         # Solana mainnet RPC endpoint
WAIAAS_RPC_EVM_ETHEREUM_MAINNET=<url>   # Ethereum mainnet RPC endpoint
Enter fullscreen mode Exit fullscreen mode

The RPC endpoint variables are worth paying attention to. WAIaaS supports 18 networks across EVM and Solana. If you're using public RPC endpoints, you're subject to their rate limits and their availability. Self-hosters who care about sovereignty often pair WAIaaS with their own RPC node or a private RPC provider configured here rather than using public defaults.

Locking Down What Your Agent Can Actually Do

Deploying securely isn't just about protecting the master password — it's also about limiting what a compromised agent session could do. WAIaaS's policy engine has 21 policy types and follows a default-deny model: transactions are blocked unless explicitly permitted.

Create a spending policy immediately after setup:

curl -X POST http://127.0.0.1:3100/v1/policies \
  -H "Content-Type: application/json" \
  -H "X-Master-Password: my-secret-password" \
  -d '{
    "walletId": "<wallet-uuid>",
    "type": "SPENDING_LIMIT",
    "rules": {
      "instant_max_usd": 100,
      "notify_max_usd": 500,
      "delay_max_usd": 2000,
      "delay_seconds": 900,
      "daily_limit_usd": 5000
    }
  }'
Enter fullscreen mode Exit fullscreen mode

This single policy creates four security tiers based on transaction amount. Anything under $100 executes instantly. $100-500 executes but sends a notification. $500-2000 is queued for 15 minutes before executing, giving you time to cancel it. Over $2000 requires explicit owner approval before it moves.

The four security tiers are: INSTANT, NOTIFY, DELAY, and APPROVAL. Without a spending policy, the default-deny rules mean token transfers are blocked unless you've also set an ALLOWED_TOKENS policy — which is actually the safer default when you're first getting set up.

Backing Up Your Wallet Data

Self-hosting means you own the backup problem too. WAIaaS includes backup commands in its CLI for exactly this:

waiaas backup create
waiaas backup list
waiaas backup inspect
Enter fullscreen mode Exit fullscreen mode

In a Docker context, your wallet data lives in the named volume waiaas-data. A complete backup strategy means both using the CLI backup commands and periodically snapshotting that volume to somewhere off the host — encrypted, of course.

The restore path is:

waiaas restore
Enter fullscreen mode Exit fullscreen mode

Test your restore process before you need it. This is advice that sounds obvious and gets ignored constantly.

What the 15-Package Monorepo Actually Runs

It's worth understanding what's inside the image you're running. WAIaaS is a 15-package monorepo: actions, adapters, admin, cli, core, daemon, desktop-spike, e2e-tests, mcp, openclaw-plugin, push-relay, sdk, shared, skills, and wallet-sdk.

The Docker deployment actually involves two images: the main WAIaaS daemon and a push-relay service. The push-relay is what enables real-time notifications when your agent triggers a transaction that requires owner approval — it's how your phone gets notified when an AI agent is trying to do something that needs your sign-off.

The daemon itself runs a 7-stage transaction pipeline: validate, auth, policy, wait, execute, and confirm stages. Every transaction your AI agent submits goes through all of these. The policy stage is where your SPENDING_LIMIT and ALLOWED_TOKENS policies get enforced. The wait stage is what implements the DELAY tier — it literally queues the transaction and waits before executing.

Quick Start: From Zero to Secured in Five Steps

If you want to skip the deep dive and just get running:

1. Clone and start:

git clone https://github.com/waiaas/WAIaaS.git
cd WAIaaS
docker compose up -d
Enter fullscreen mode Exit fullscreen mode

2. Create a wallet:

curl -X POST http://127.0.0.1:3100/v1/wallets \
  -H "Content-Type: application/json" \
  -H "X-Master-Password: my-secret-password" \
  -d '{"name": "trading-wallet", "chain": "solana", "environment": "mainnet"}'
Enter fullscreen mode Exit fullscreen mode

3. Create a session for your AI agent:

curl -X POST http://127.0.0.1:3100/v1/sessions \
  -H "Content-Type: application/json" \
  -H "X-Master-Password: my-secret-password" \
  -d '{"walletId": "<wallet-uuid>"}'
Enter fullscreen mode Exit fullscreen mode

4. Set a spending policy (do this before funding the wallet):
Use the policy creation curl from the previous section with limits appropriate for your use case.

5. Move secrets to Docker Secrets:

mkdir -p secrets
echo "your-secure-password" > secrets/master_password.txt
chmod 600 secrets/master_password.txt
docker compose -f docker-compose.yml -f docker-compose.secrets.yml up -d
Enter fullscreen mode Exit fullscreen mode

What's Next

The self-hosted path gives you complete ownership, but it does require you to think through operational details — secrets management, backups, network access controls — that a hosted service would handle for you. The tradeoff is real, and it's worth it if key custody matters to you.

WAIaaS has 684+ test files across its packages and an interactive API reference at http://127.0.0.1:3100/reference once you're running, which makes exploring the full API surface much easier than reading raw documentation. The OpenAPI 3.0 spec is downloadable at /doc if you want to generate client code or explore the 39 REST API route modules.

If you run into issues, the GitHub repository is the right place — the issue tracker and discussions are active, and since it's open source, you can read exactly what the entrypoint script does rather than trusting that it does the right thing.


Get started with WAIaaS:

Top comments (0)