Skip to main content
See also: Production Deployment for the two-server production topology.

Local Development

That’s it. Vault is automatically started, configured, and seeded. No manual .env setup needed.

What Happens on Startup

  1. Infrastructure starts: PostgreSQL, Redis, LocalStack (S3), Temporal
  2. Vault starts in dev mode (in-memory, auto-unseal, no TLS)
  3. vault-dev-init.sh runs via the vault-init sidecar:
    • Enables KV v2 and Transit secret engines
    • Creates AppRole hoodcloud-dev with permissive dev policy
    • Seeds master encryption key, JWT RSA keys, ECDSA signing key, X25519 sealed box keypair, dev passwords
    • Creates Transit key hoodcloud-master (aes256-gcm96)
    • Writes role-id and secret-id to /vault/config/ shared volume
  4. Control plane services start, reading credentials from the shared volume
  5. Idempotent, completes in ~5 seconds

Vault UI

Dev Notes

  • Data is ephemeral (Vault dev mode uses in-memory storage)
  • No TLS (VAULT_TLS_SKIP_VERIFY=true on all services)
  • The same vault-dev-init.sh is used in E2E tests (tests/e2e/docker-compose.e2e.yml) on port 8201

Single-Server Staging

For staging or internal testing, the entire stack (control plane + observability) runs on a single VM.

Architecture

Prerequisites

  1. Hetzner Cloud VM: cx31 minimum (4 vCPU, 8GB RAM), Ubuntu 22.04
  2. AWS resources: S3 buckets + DynamoDB table (see Production Deployment - AWS Resources)
  3. DNS: api., auth., grafana. subdomains pointing to the VM

Setup

Required .env values: see Environment Variables.

Generate Certificates

Deploy

Important: Migrations must run before application services start. The cmd/migrate binary is idempotent — safe to run multiple times. Services no longer run migrations at startup.

Verify

Note: /health returns only top-level status. Use /readyz for detailed component health.

Internal Ports


Day-to-Day Operations

View Logs

Restart Services

Important: restart does not reload .env changes. Use up -d to pick up env var updates.

Update Deployment

Release Chain Configs

When chain config files are updated in the hoodcloud-chain-configs repo:
This triggers the GitHub Actions workflow:
  1. Validates config files against schemas
  2. Creates tarball + checksums
  3. Uploads to S3 (s3://hoodcloud-chain-configs/v1.1.0/)
  4. Copies to latest/ for auto-update mode
  5. Creates GitHub release
Auto-reload (default): Services using CHAIN_PROFILE_VERSION=latest poll checksums.txt at CHAINS_CHECK_INTERVAL (default: 1m). Config swaps atomically, no restart needed.
Pinned version (controlled rollouts):
Ops-agent config distribution:

Database Backup & Restore

Secrets Rotation

After updating secrets in Vault:
See also: Vault Operations for secret rotation and AppRole credential management.

Upgrade Rollout Operations

Create and Start a Rollout

Two modes are available for rollout creation: Auto mode (recommended): Provide upgrade_id — the control plane loads the upgrade manifest from chain-configs and auto-populates binary coordinates, state_compatibility, config_changes, and manifest_content_hash.
Manual mode: Omit upgrade_id — operator provides all fields explicitly.

Monitor Rollout Progress

Pause, Resume, Cancel

Rollback

Multi-Binary Rollout (Rollout Groups)

For chains with multiple binaries (e.g., ethereum-holesky: geth + lighthouse):

Database Queries (Rollout Debugging)


Rolling Uptime

The UptimeWorker runs inside the health-evaluator process — no separate deployment needed.

How It Works

  • State transitions are logged to node_state_log (append-only) via the StateLogHandler registered on CompositeTransitionHandler
  • The UptimeWorker materializes hourly uptime buckets (node_uptime_hourly) every 5 minutes
  • API endpoints read pre-computed buckets for fast queries

Configuration Defaults

No environment variables are required. Defaults are defined in internal/defaults/defaults.go:

API Endpoints

Both require nodes:read scope + ownership verification:
  • GET /api/v1/nodes/{nodeID}/uptime?window={24h|7d|30d} — Rolling uptime summary
  • GET /api/v1/nodes/{nodeID}/uptime/history?window={24h|7d|30d}&granularity={hourly|daily} — Per-period breakdown

Operational Characteristics

  • Idempotent: Safe to restart. The worker catches up from the last complete bucket.
  • Self-healing: Individual node failures are logged but don’t block other nodes.
  • Auto-purge: Buckets older than 90 days are automatically deleted.
  • No external cursor: Progress is implicit in is_complete flags on hourly buckets.

Monitoring

The worker logs warnings on per-node failures:
Key log patterns:
  • "Failed to compute uptime for node" — Per-node error (non-blocking)
  • "Failed to compute uptime batch" — Batch-level error (continues to next batch)

Database Queries (Debugging)

Manual Bucket Recomputation

If a bug produced incorrect buckets, mark affected buckets for recomputation:
The worker recomputes these on its next tick.

Leader Election (Health Evaluator)

The health evaluator uses PostgreSQL advisory lock-based leader election. When running 2+ instances, one acquires the lock and runs evaluation cycles. The others are hot standby.

How It Works

  • Leader acquires advisory lock via a dedicated, non-pooled pgx.Conn
  • Lock is held for the lifetime of leader tenure (connection stays open)
  • On leader failure, a standby acquires the lock within 15-30 seconds
  • Leader-gated goroutines: evaluation, subscription cleanup, terraform cleanup, maintenance recovery, S3 backup cleanup, incident dispatcher, rate limiter eviction, uptime worker, policy evaluation (9 of 11 total)
  • NOT leader-gated: outbox worker (uses FOR UPDATE SKIP LOCKED), metrics ingester (idempotent writes)

Monitoring

Alert: If health_evaluator_is_leader == 0 across ALL instances for > 60s, no instance holds the lock. Check PostgreSQL connectivity.

Troubleshooting

No leader for > 60 seconds:
  1. Check PostgreSQL connectivity from all health evaluator instances
  2. Check advisory lock status:
  3. Check if a stale connection holds the lock:
  4. If a stale backend holds the lock, terminate it:
Failover behavior: When the leader dies, its PostgreSQL connection closes and the advisory lock is released. A standby acquires the lock on its next retry interval (default 5s). Maximum monitoring gap during failover: one evaluation cycle (15-30s).

Auth Server Rate Limiting

The auth server uses Redis-backed rate limiting (same implementation as the API server). This ensures rate limits are enforced globally across multiple auth server instances.
  • Circuit breaker fallback: If Redis is unavailable, falls back to in-memory rate limiting per instance
  • Configuration: AUTH_RATE_LIMIT_PER_MINUTE (default: 20)
  • Requires: Redis connection (REDIS_HOST)

Runbooks

Cold Start / Disaster Recovery

Full system restart sequence:
  1. Start infrastructure: PostgreSQL, Redis, NATS
  2. Wait for PostgreSQL to be ready (accept connections)
  3. Run cmd/migrate to apply any pending migrations
  4. Start Temporal server, wait for it to be ready
  5. Start application services: auth-server, api-server, agent-gateway, orchestrator, health-evaluator
  6. Verify health endpoints respond
  7. Verify health evaluator leader election (health_evaluator_is_leader metric)

Dirty Migration Recovery

If cmd/migrate fails mid-migration, the schema_migrations table is marked dirty=true. Subsequent migration runs will refuse to proceed.
  1. Identify the failed migration:
  2. Assess the damage: Check if the migration partially applied:
  3. Fix the state:
    • If the migration fully applied but the dirty flag wasn’t cleared:
    • If the migration partially applied, manually complete or revert it, then update the version:
  4. Re-run migrations:

Health Evaluator Leader Troubleshooting

See Leader Election - Troubleshooting above.

PostgreSQL Failover Verification

After a managed PostgreSQL failover event:
  1. Verify all services reconnected:
  2. Check connection pool health:
  3. Verify health evaluator re-acquired leader lock after failover
  4. Check for any statement timeout spikes during failover window

Redis Failover Verification

After a Redis Sentinel failover:
  1. Verify command queue is operational (agents receiving commands)
  2. Verify rate limiter circuit breaker recovered:
  3. Check auth server rate limiter recovered (same circuit breaker pattern)

Connection Pool Tuning

When db_pool_active_connections > 80% of MaxConns or db_pool_acquire_duration_seconds p99 > 1s:
  1. Identify which service is exhausting its pool
  2. Check if slow queries are holding connections (see Statement Timeout Runbook in Database Guardrails)
  3. Increase DB_MAX_CONNS for the affected service
  4. Verify the total connection budget stays under PostgreSQL max_connections
  5. If total budget exceeds limits, consider deploying PgBouncer

Query Performance Degradation

When p95 SLO is breached:
  1. Identify the degraded service from hoodcloud_db_query_duration_seconds metric
  2. Query pg_stat_statements for the top queries by mean execution time:
  3. Run EXPLAIN ANALYZE on the slow query
  4. Check for missing indexes, table bloat, or lock contention
  5. If the health evaluator’s ListSnapshots query is degrading, check node count growth

Troubleshooting

Caddy Certificate Issues

  1. Verify DNS records: dig api.hoodcloud.io
  2. Check ports 80 and 443 are open
  3. Check logs: docker compose logs caddy

Database Connection Issues

Temporal Issues

API Server Won’t Start - Missing Binaries Dir

The API server builds ops-agent into /app/binaries/ during Docker build. Rebuild if missing:

Services Fail After Vault Restart

Vault seals on every restart. Unseal with 3 of 5 keys before services can authenticate. See Vault Operations.

Database Errors After Secrets Rotation


Security Notes

  • All internal services bind to 127.0.0.1 only
  • External access only through Caddy reverse proxy (auto TLS)
  • gRPC uses self-signed CA for ops-agent authentication
  • Security boundaries: see CLAUDE.md