DEV Community

Cover image for Auto-Updating Self-Hosted Crypto Wallets: Watchtower + GHCR Production Setup
Wallet Guy
Wallet Guy

Posted on

Auto-Updating Self-Hosted Crypto Wallets: Watchtower + GHCR Production Setup

Auto-Updating Self-Hosted Crypto Wallets: Watchtower + GHCR Production Setup

Self-hosting your AI agent's wallet infrastructure means you never have to ask: "Who actually controls these private keys?" If you've been running homelab services for a while, you already know the value of owning your stack. This guide walks through a production-ready WAIaaS deployment with automatic Docker image updates via Watchtower and GitHub Container Registry (GHCR) — so you get the control of self-hosting without the maintenance burden of manual updates.

Why Self-Hosting Your Agent's Wallet Actually Matters

When an AI agent moves real money — swapping tokens, paying for API calls, interacting with DeFi protocols — the custody question isn't academic. Hosted wallet services mean someone else's server holds the keys, enforces the rate limits, and logs the transactions. That's a reasonable trade-off for prototyping, but it's a meaningful one.

Running WAIaaS on your own hardware means your keys stay on your hardware. The daemon runs locally, the database lives in a named Docker volume you control, and nothing phones home unless you've configured an RPC endpoint that does. It's the crypto equivalent of running your own email server — except WAIaaS has a one-command quick start, so it's actually practical.

The production concern that kills most self-hosted setups isn't the initial deployment — it's the update treadmill. Security patches, new DeFi integrations, bug fixes: staying current manually is tedious. That's where Watchtower comes in.

What You're Building

By the end of this guide you'll have:

  • WAIaaS daemon running in Docker, bound to 127.0.0.1:3100 (not exposed to the internet)
  • Automatic image updates pulling from ghcr.io/waiaas/waiaas:latest
  • Production secrets managed via Docker Secrets (no passwords in environment variables)
  • A healthcheck that restarts the container if the daemon goes unresponsive

The full WAIaaS stack is a 15-package monorepo with a 7-stage transaction pipeline, 21 policy types, and 45 MCP tools for AI agent integration. You don't need to understand all of that to run it — but it's good to know the depth is there when you need it.

Step 1: Clone and Understand the Compose File

git clone https://github.com/waiaas/WAIaaS.git
cd WAIaaS
Enter fullscreen mode Exit fullscreen mode

The repository ships with a docker-compose.yml that's already production-shaped. Here's what it looks like, annotated:

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 for the self-hoster:

Port binding is localhost-only. 127.0.0.1:3100:3100 means the API is not reachable from outside the host. If you want to access it from another machine, you handle that at the reverse-proxy layer (nginx, Caddy, Tailscale) — not by changing this binding to 0.0.0.0.

Named volume, not a bind mount. waiaas-data is managed by Docker, which keeps things clean. Your wallet data, session tokens, and configuration survive container restarts and image updates. docker compose down preserves the volume; docker compose down -v deletes it — don't run the latter in production.

Healthcheck is built in. The daemon exposes /health, and Docker will automatically restart the container if it stops responding. No external monitoring required for basic uptime.

Step 2: First Boot with Auto-Provision

For a first deployment, auto-provision is the cleanest path. It generates a random master password and writes it to /data/recovery.key inside the container:

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

# Retrieve auto-generated master password
docker exec waiaas cat /data/recovery.key
Enter fullscreen mode Exit fullscreen mode

Save that password somewhere secure (a password manager, not a sticky note). Once you've noted it, you can harden the setup using waiaas set-master from the CLI if you have it installed, then delete the recovery key file.

For most homelab setups, the CLI-based flow is even cleaner:

npm install -g @waiaas/cli
waiaas init --auto-provision     # Generates random master password → recovery.key
waiaas start                     # No password prompt
waiaas quickset                  # Creates wallets + sessions automatically
waiaas set-master                # (Later) Harden password, then delete recovery.key
Enter fullscreen mode Exit fullscreen mode

The CLI has 20 commands covering everything from backup create to wallet info. The quickset command is particularly useful — it creates wallets and MCP sessions in one step, printing the Claude Desktop config JSON you can paste directly.

Step 3: Production Secrets (No Passwords in Environment Variables)

Environment variables are convenient but they leak into docker inspect, process lists, and log aggregators. For a production self-hosted setup, Docker Secrets are the right tool.

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

# Create secret files
mkdir -p secrets
echo "your-secure-password" > secrets/master_password.txt
chmod 600 secrets/master_password.txt

# Deploy with secrets overlay
docker compose -f docker-compose.yml -f docker-compose.secrets.yml up -d
Enter fullscreen mode Exit fullscreen mode

The entrypoint script in the Docker image handles reading from Docker Secrets automatically — that's a built-in capability of the entrypoint, not something you need to wire up yourself.

Step 4: Watchtower for Automatic Updates

This is the part that makes self-hosting sustainable. Watchtower watches your running containers, checks their registries for new image versions, and performs rolling updates automatically.

Add Watchtower to your compose file:

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
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3100/health"]
      interval: 30s
      timeout: 5s
      start_period: 10s
      retries: 3

  watchtower:
    image: containrrr/watchtower
    container_name: watchtower
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
    environment:
      - WATCHTOWER_CLEANUP=true
      - WATCHTOWER_POLL_INTERVAL=86400
    restart: unless-stopped

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

WATCHTOWER_POLL_INTERVAL=86400 checks for updates once a day. WATCHTOWER_CLEANUP=true removes old image layers after updating, keeping your disk tidy.

The WAIaaS image pulls from ghcr.io/waiaas/waiaas:latest (GitHub Container Registry), which is public and doesn't require authentication for pulls. Watchtower will detect new pushes to that tag and update your local container automatically — with a graceful restart that preserves the named volume and all your wallet data.

If you'd rather pin to a specific version and update manually, swap :latest for a specific tag. That's a reasonable choice if you want to review release notes before updating a production wallet daemon.

Step 5: Configure Your RPC Endpoints

By default, WAIaaS needs RPC endpoints to talk to blockchain networks. The key environment variables:

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

Put these in a .env file in your project directory (it's in .gitignore by default). The daemon supports 2 chain types — Solana and EVM — across 18 networks total. You only need to configure the ones you're actually using.

For a privacy-conscious setup, this is where self-hosting really shines. You can point to your own RPC node, a private endpoint from a provider you trust, or a local node if you're running one. No third-party wallet service is sitting between your agent and the chain.

Step 6: Create a Wallet and Lock It Down with Policies

Once the daemon is running, 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

Then create a session token 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

The agent uses the session token (wai_sess_...) for all its operations. Your master password never leaves your server.

Before letting an agent loose with real funds, set up a spending policy. The policy engine has 21 policy types with 4 security tiers (INSTANT, NOTIFY, DELAY, APPROVAL) and enforces default-deny — transactions are blocked unless explicitly permitted:

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 means: transactions under $100 execute immediately, $100–$500 execute with a notification to you, $500–$2000 are queued for 15 minutes (cancellable), and anything over $2000 requires your explicit approval via WalletConnect or Telegram.

The default-deny enforcement on ALLOWED_TOKENS and CONTRACT_WHITELIST means if you haven't explicitly whitelisted a token or contract, the transaction is denied. An agent can't spend funds on something you haven't approved.

Checking That Everything Works

# Follow logs in real time
docker compose logs -f

# Verify the healthcheck is passing
docker inspect waiaas-daemon --format='{{.State.Health.Status}}'

# Check your wallet balance from the agent's perspective
curl http://127.0.0.1:3100/v1/wallet/balance \
  -H "Authorization: Bearer wai_sess_eyJhbGciOiJIUzI1NiJ9..."

# View the interactive API reference (if you want to explore)
open http://127.0.0.1:3100/reference
Enter fullscreen mode Exit fullscreen mode

The /reference endpoint serves an interactive Scalar API reference UI auto-generated from the OpenAPI 3.0 spec. There are 39 REST API route modules in total — the reference UI is the fastest way to explore them without reading source code.

The Management Commands You'll Actually Use

docker compose up -d          # Start daemon
docker compose logs -f        # Follow logs
docker compose down           # Stop (data preserved in named volume)
docker compose down -v        # Stop + delete data volume (careful)
Enter fullscreen mode Exit fullscreen mode

For CLI-based management, waiaas status and waiaas wallet info cover most day-to-day checks. The waiaas backup create / waiaas backup list / waiaas restore commands handle your disaster recovery workflow.

The Architecture in Brief

What you're running is a daemon with a 7-stage transaction pipeline: validate → auth → policy → wait → execute → confirm. The policy check at stage 3 is where your spending limits, whitelists, and approval requirements get enforced. The "wait" stage handles the delay-tier queuing. Nothing reaches the chain without passing all prior stages.

The daemon supports 15 DeFi protocol integrations (Aave v3, Jupiter, Hyperliquid, Lido, Jito, and others), 45 MCP tools for connecting AI agents via Claude Desktop, and 7 transaction types (Transfer, TokenTransfer, ContractCall, Approve, Batch, NftTransfer, ContractDeploy). All of that runs in a single Docker container on your hardware.

For AI agent integration via the Model Context Protocol, the setup is a single CLI command after the daemon is running:

waiaas mcp setup --all    # Auto-register all wallets with Claude Desktop
Enter fullscreen mode Exit fullscreen mode

Your Keys, Your Server, Your Rules

The philosophy here is straightforward: a self-hosted wallet daemon gives you the same trust model as running your own Bitcoin node. You verify things yourself. You don't depend on a third party's uptime, pricing decisions, or data retention policies.

With Watchtower handling updates from GHCR and Docker Secrets keeping your master password out of environment variables, you get a production setup that's both sovereign and maintainable. The 684+ test files in the codebase mean updates hitting :latest have been through a real test suite before you pull them.

The full source is at https://github.com/waiaas/WAIaaS — worth reading the Docker entrypoint and compose files yourself before running anything in production. And the project home with documentation is at https://waiaas.ai.

If you're already comfortable self-hosting services, the hardest part of this setup is probably choosing which DeFi protocols to whitelist — not the deployment itself.

Top comments (0)