Before you start, make sure you have:
- Docker (version 20.10+)
- Docker Compose (version 2.0+)
- Git
- Code editor (VS Code, GoLand, etc.)
- Go 1.21+ (optional, for running without Docker)
- curl or Postman (for testing API)
git clone https://github.com/Emengkeng/vaultkey
cd vaultkeycp .env.example .envEdit .env:
# Simple defaults for local dev - DO NOT use in production
POSTGRES_PASSWORD=dev_password_123
REDIS_PASSWORD=dev_redis_456
WORKER_CONCURRENCY=5 # Lower for local dev
# Testnet RPCs (free, no API key needed)
EVM_RPC_11155111=https://eth-sepolia.public.blastapi.io
EVM_RPC_80001=https://rpc-mumbai.maticvigil.com
SOLANA_RPC_URL=https://api.devnet.solana.comdocker compose up -dWhat happens:
- PostgreSQL starts (database)
- Redis starts (queue)
- Vault starts (key management)
- Vault auto-initializes and unseals
- API + Workers start
First run takes ~2 minutes (downloading images)
If you see permission denied errors from Vault:
# Fix vault volume permissions manually
sudo chown -R 100:100 /var/lib/docker/volumes/vaultkey_vault_data/_data
# Restart
docker compose down
docker compose up -ddocker compose logs vault-initYou'll see:
==========================================================
VAULT INITIALIZED - SAVE THESE KEYS SECURELY
==========================================================
Unseal Key 1: abc123...
Unseal Key 2: def456...
Unseal Key 3: ghi789...
Root Token: xyz...
==========================================================
Save these to a text file locally - You'll need them if Vault restarts.
# Health check
curl http://localhost:8080/health
# Should return: {"vault":"ok","redis":"ok"}
# Create a project
curl -X POST http://localhost:8080/projects \
-H "Content-Type: application/json" \
-d '{
"name": "Local Dev",
"webhook_url": "https://webhook.site/YOUR_URL",
"rate_limit_rps": 100
}'Save the api_key and api_secret from the response!
# Replace YOUR_API_KEY and YOUR_API_SECRET
curl -X POST http://localhost:8080/wallets \
-H "Content-Type: application/json" \
-H "X-API-Key: YOUR_API_KEY" \
-H "X-API-Secret: YOUR_API_SECRET" \
-d '{
"user_id": "dev_user_1",
"chain_type": "evm",
"label": "test_wallet"
}'✅ You're ready to develop!
The Docker setup does NOT have hot reloading by default. When you change Go code, you need to rebuild.
# After making code changes
docker compose build api
docker compose restart apiRebuilds only the API container (~20 seconds)
docker compose down
docker compose up -ddocker compose down
docker compose build
docker compose up -dvaultkey/
├── cmd/
│ └── api/
│ └── main.go # Entry point
├── internal/
│ ├── api/
│ │ ├── handlers/ # HTTP handlers
│ │ └── middleware/ # Auth, rate limiting
│ ├── kms/ # Vault integration
│ ├── nonce/ # Nonce management
│ ├── queue/ # Redis queue
│ ├── ratelimit/ # Rate limiter
│ ├── relayer/ # Gasless transactions
│ ├── rpc/ # Blockchain RPC
│ ├── storage/ # Database
│ ├── wallet/ # Wallet generation & signing
│ ├── webhook/ # Webhook delivery
│ └── worker/ # Job processing
├── config/
│ └── config.go # Configuration loading
├── migrations/
│ └── schema.sql # Database schema
├── scripts/
│ └── vault-init.sh # Vault initialization
├── docker-compose.yml # Docker services
├── Dockerfile # API container
├── .env # Environment variables
├── .env.example # Template
└── README.md
All services:
docker compose logs -fSpecific service:
docker compose logs -f api # API + Workers
docker compose logs -f postgres # Database
docker compose logs -f redis # Queue
docker compose logs -f vault # KMSFilter by time:
docker compose logs --since 5m api
docker compose logs --tail 100 api# PostgreSQL shell
docker compose exec postgres psql -U vaultkey -d vaultkey
# Useful queries
SELECT COUNT(*) FROM wallets;
SELECT COUNT(*), status FROM signing_jobs GROUP BY status;
SELECT * FROM audit_log ORDER BY created_at DESC LIMIT 10;GUI Tool (Optional):
- Download TablePlus, DBeaver, or pgAdmin
- Connect to:
localhost:5432 - Database:
vaultkey - Username:
vaultkey - Password: (from
.env)
# Redis CLI
docker compose exec redis redis-cli -a dev_redis_456
# Useful commands
> LLEN vaultkey:jobs # Queue depth
> LRANGE vaultkey:jobs 0 -1 # View all jobs
> KEYS vaultkey:* # All VaultKey keys
> DEL vaultkey:nonce:1:0x... # Reset nonce (if stuck)# Vault status
docker compose exec vault vault status -address=http://127.0.0.1:8200
# List encryption keys
docker compose exec vault vault list -address=http://127.0.0.1:8200 transit/keys
# Unseal manually (if needed)
docker compose exec vault vault operator unseal -address=http://127.0.0.1:8200If you prefer to run Go directly (faster iteration):
# Run only Postgres, Redis, Vault
docker compose up -d postgres redis vault vault-initexport DATABASE_URL="postgres://vaultkey:dev_password_123@localhost:5432/vaultkey?sslmode=disable"
export VAULT_ADDR="http://localhost:8200"
export VAULT_TOKEN="$(cat ./vault-data/root_token)" # Or use the token from logs
export REDIS_ADDR="localhost:6379"
export REDIS_PASSWORD="dev_redis_456"
export WORKER_CONCURRENCY=5
export EVM_RPC_11155111="https://eth-sepolia.public.blastapi.io"
export SOLANA_RPC_URL="https://api.devnet.solana.com"# Install dependencies
go mod download
# Run
go run cmd/api/main.goAdvantages:
- ✅ Instant restarts (no rebuild)
- ✅ Native debugger support
- ✅ Faster compilation
Disadvantages:
- ❌ Need Go installed locally
- ❌ Need to manage environment variables
- ❌ Ports might conflict (8080, 5432, 6379, 8200)
Install: Remote - Containers extension
.vscode/launch.json:
{
"version": "0.2.0",
"configurations": [
{
"name": "Attach to Docker",
"type": "go",
"request": "attach",
"mode": "remote",
"remotePath": "/app",
"port": 2345,
"host": "localhost"
}
]
}Dockerfile (add Delve):
RUN go install github.com/go-delve/delve/cmd/dlv@latest
CMD ["dlv", "debug", "--headless", "--listen=:2345", "--api-version=2", "--accept-multiclient", "./cmd/api"]Expose port in docker-compose.yml:
api:
ports:
- "8080:8080"
- "2345:2345" # Debugger- Run → Edit Configurations
- Add → Go Remote
- Host:
localhost, Port:2345 - Set breakpoints in code
- Click Debug
log.Printf("[DEBUG] Wallet ID: %s, Chain: %s", walletID, chainID)View logs:
docker compose logs -f api | grep DEBUGPostman Collection: Create a collection with your API requests. Import/export for team sharing.
Example request (Postman):
POST http://localhost:8080/wallets
Headers:
Content-Type: application/json
X-API-Key: {{api_key}}
X-API-Secret: {{api_secret}}
Body:
{
"user_id": "test_user",
"chain_type": "evm"
}
cURL Scripts:
Create a test.sh file:
#!/bin/bash
API_KEY="vk_live_..."
API_SECRET="vk_secret_..."
# Create wallet
WALLET=$(curl -s -X POST http://localhost:8080/wallets \
-H "Content-Type: application/json" \
-H "X-API-Key: $API_KEY" \
-H "X-API-Secret: $API_SECRET" \
-d '{
"user_id": "test_user",
"chain_type": "evm"
}')
echo "Wallet created: $WALLET"
WALLET_ID=$(echo $WALLET | jq -r '.id')
# Check balance
curl -s "http://localhost:8080/wallets/$WALLET_ID/balance?chain_id=11155111" \
-H "X-API-Key: $API_KEY" \
-H "X-API-Secret: $API_SECRET" | jqMake executable:
chmod +x test.sh
./test.sh# Run all tests
go test ./...
# Run specific package
go test ./internal/wallet
# With coverage
go test -cover ./...
# Verbose output
go test -v ./internal/walletExample test:
// internal/api/handlers/handlers_test.go
package handlers_test
import (
"testing"
"net/http/httptest"
)
func TestCreateWallet(t *testing.T) {
// Setup test database
// Setup test handlers
// Make request
// Assert response
}Error:
Error starting userland proxy: listen tcp4 0.0.0.0:8080: bind: address already in use
Solution:
# Find what's using the port
lsof -i :8080
# Or on Linux:
netstat -tulpn | grep 8080
# Kill the process
kill -9 <PID>
# Or change the port in docker-compose.yml
ports:
- "8081:8080" # Use 8081 insteadError:
{"vault":"Vault is sealed","redis":"ok"}
Solution:
# Run the init script again
docker compose restart vault-init
# Or manually
docker compose logs vault-init # Get unseal keys
docker compose exec vault vault operator unseal <key1>
docker compose exec vault vault operator unseal <key2>Error:
pq: relation "wallets" does not exist
Solution:
# Schema is applied on first run only
# To reapply:
docker compose down -v # Delete volumes (LOSES ALL DATA)
docker compose up -dOr manually:
docker compose exec postgres psql -U vaultkey -d vaultkey -f /docker-entrypoint-initdb.d/schema.sqlProblem: Changed code but API still behaves the same
Solution:
# Rebuild the container
docker compose build api
docker compose restart api
# Or run without Docker (see "Running Without Docker" section)Symptoms:
- Jobs stuck in "pending"
LLEN vaultkey:jobskeeps growing
Debug:
# Check worker logs
docker compose logs -f api | grep worker
# Check queue depth
docker compose exec redis redis-cli -a dev_redis_456 LLEN vaultkey:jobs
# Restart workers
docker compose restart apiError:
no space left on device
Solution:
# Clean up Docker
docker system prune -a --volumes
# Remove old images
docker image prune -a
# Check disk usage
docker system dfCreate Makefile:
.PHONY: start stop restart logs build test
start:
docker compose up -d
stop:
docker compose down
restart:
docker compose restart api
logs:
docker compose logs -f api
build:
docker compose build api
docker compose restart api
test:
go test ./...
clean:
docker compose down -v
docker system prune -fUsage:
make start
make logs
make build
make testAdd to ~/.bashrc or ~/.zshrc:
alias vk-start='docker compose up -d'
alias vk-stop='docker compose down'
alias vk-logs='docker compose logs -f api'
alias vk-rebuild='docker compose build api && docker compose restart api'
alias vk-db='docker compose exec postgres psql -U vaultkey -d vaultkey'
alias vk-redis='docker compose exec redis redis-cli -a dev_redis_456'
alias vk-health='curl http://localhost:8080/health'Install air for hot reloading:
# Install air
go install github.com/cosmtrek/air@latest
# Create .air.toml
air init
# Run with air instead of go run
airdocker-compose.yml:
services:
api:
# ... regular config ...
api-debug:
profiles: ["debug"]
# ... same as api but with delve ...
mailhog: # Email testing
profiles: ["mail"]
image: mailhog/mailhog
ports:
- "8025:8025"Usage:
# Normal mode
docker compose up -d
# Debug mode
docker compose --profile debug up -d
# With email testing
docker compose --profile mail up -dCreate scripts/seed.sh:
#!/bin/bash
set -e
API_KEY=${1}
API_SECRET=${2}
if [ -z "$API_KEY" ]; then
echo "Usage: ./seed.sh <api_key> <api_secret>"
exit 1
fi
echo "Creating 10 test wallets..."
for i in {1..10}; do
curl -s -X POST http://localhost:8080/wallets \
-H "Content-Type: application/json" \
-H "X-API-Key: $API_KEY" \
-H "X-API-Secret: $API_SECRET" \
-d "{
\"user_id\": \"user_$i\",
\"chain_type\": \"evm\",
\"label\": \"wallet_$i\"
}" | jq -r '.address'
done
echo "Done! Created 10 wallets."Usage:
chmod +x scripts/seed.sh
./scripts/seed.sh YOUR_API_KEY YOUR_API_SECRET.env.local:
POSTGRES_PASSWORD=dev_password
REDIS_PASSWORD=dev_redis
WORKER_CONCURRENCY=5
# Testnet RPCs (free)
EVM_RPC_11155111=https://eth-sepolia.public.blastapi.io
SOLANA_RPC_URL=https://api.devnet.solana.com.env.local.mainnet:
POSTGRES_PASSWORD=dev_password
REDIS_PASSWORD=dev_redis
WORKER_CONCURRENCY=5
# Mainnet RPCs (use your Alchemy/Infura keys)
EVM_RPC_1=https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY
SOLANA_RPC_URL=https://api.mainnet-beta.solana.comSwitch environments:
cp .env.local .env && docker compose restart api
cp .env.local.mainnet .env && docker compose restart api# Container stats (live)
docker stats
# Disk usage
docker system dfAdd to docker-compose.yml:
prometheus:
image: prom/prometheus
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
ports:
- "9090:9090"
grafana:
image: grafana/grafana
ports:
- "3000:3000"Access Grafana at http://localhost:3000
Problem: Each developer has their own Vault keys and database
Solution:
-
Shared Vault keys (optional):
- Commit
vault_init.jsonto private repo (never public!) - Team members copy file before first run
- Commit
-
Shared database snapshots:
# Export
docker compose exec postgres pg_dump -U vaultkey vaultkey > snapshot.sql
# Import
cat snapshot.sql | docker compose exec -T postgres psql -U vaultkey -d vaultkey- Git ignore patterns:
# .gitignore
.env
vault-data/
postgres-data/
redis-data/
*.log# Morning: Start everything
docker compose up -d
curl http://localhost:8080/health # Verify
# Make code changes
# ... edit files in your IDE ...
# Rebuild and test
docker compose build api
docker compose restart api
curl -X POST http://localhost:8080/wallets ... # Test
# Check logs if something breaks
docker compose logs -f api
# End of day: Stop everything (optional)
docker compose downThat's it! Docker handles all the complexity - you just need:
docker compose up -dto startdocker compose build api && docker compose restart apiafter code changesdocker compose logs -f apito debug
Simple and clean! 🚀