Skip to main content
See also: Deployment & Operations for local dev and single-server staging.
From-scratch guide for deploying the full HoodCloud stack across two servers.

Server Topology

Communication between servers:
  • gRPC + mTLS (control plane -> payment service, port 50051)
  • NATS JetStream (payment service -> control plane, port 4223)

Prerequisites

  • Two servers: Ubuntu 22.04+, 4 vCPU / 8GB RAM minimum each
  • DNS records pointing to control plane IP: api., auth., grafana., status. subdomains
  • AWS account with IAM user (S3, DynamoDB)
  • Hetzner Cloud API token
  • GitHub repo access

Part 1: AWS Resources

S3 Buckets

DynamoDB (Terraform Locks)

IAM Policy

Save the access key credentials for .env configuration. See infrastructure/iam/README.md for full documentation. GitHub OIDC for Chain Configs (optional): For automated chain config releases via GitHub Actions, set up OIDC federation. See Deployment & Operations - Release Chain Configs for the release workflow.

Part 1b: Vault

Vault is the secrets provider for all application secrets.
Full guide: Vault Operations covers setup, initialization, secret population, and day-to-day operations.
Quick sequence:
  1. Generate TLS certs: VAULT_SERVER_IP=<ip> ./scripts/generate-vault-certs.sh
  2. Deploy Vault: docker compose -f docker-compose.vault.yml --profile vault-prod up -d vault
  3. Initialize: vault operator init -key-shares=5 -key-threshold=3
  4. Unseal (3 of 5 keys)
  5. Run init script: ./scripts/vault-init.sh
  6. Populate secrets: ./scripts/vault-migrate-secrets.sh --manual
  7. Store Cloudflare API token: vault kv put secret/infra/certbot/cloudflare api_token=<cloudflare-api-token>
  8. Deploy AppRole credentials to control plane
  9. Revoke root token

Part 2: Control Plane Server

2.1 Server Setup

2.2 Clone Repository

2.3 Generate Certificates

Deploy payment client certs to control plane:
Note: Key files need 644 permissions because containers run as non-root users. The host directory (/opt/hoodcloud/secrets/, owned by root) provides access control.

2.4 Deploy Vault Credentials

2.5 Configure Environment

Edit .env with production values. The file covers: AWS credentials, domain names, database, NATS, authentication (Clerk Setup), chain configs, Terraform state, Vault (Vault Operations), and payment service mTLS.
Full variable reference with defaults and descriptions: Environment Variables

2.5b NATS TLS (Let’s Encrypt)

NATS uses Let’s Encrypt TLS via certbot DNS-01 challenge (Cloudflare). The nats.hoodcloud.io Docker network alias is configured in docker-compose.yml so internal services verify the certificate using the same hostname as the LE certificate’s CN. This is why NATS_URL=tls://nats.hoodcloud.io:4222 works both internally (via Docker network alias) and externally (via DNS). Install certbot:
Deploy scripts from repo to server:
Issue initial certificate:
Create deploy hook symlink:
Override certbot systemd timer to use Vault wrapper:
Verify:

2.6 Deploy Control Plane

Important: The cmd/migrate step must run before application services. It is idempotent and exits non-zero on failure. If migration fails, do NOT start application services — see Runbooks for recovery.

2.7 Verify Control Plane


Part 3: Payment Service Server

3.1 Server Setup

3.2 Clone & Deploy Certs

3.3 Configure Environment

3.4 Deploy

3.5 Verify


Part 4: End-to-End Verification

mTLS Handshake

From the control plane server:

Service Health Matrix

Pre-deploy checklist: Deployment Checklist

Port Reference

Control Plane

Payment Service


Stripe Setup

1. Configure Stripe Dashboard

Create a webhook endpoint in Stripe:
  • URL: https://pay.hoodcloud.io/webhooks/stripe
  • Events: checkout.session.completed, checkout.session.async_payment_succeeded, checkout.session.async_payment_failed, checkout.session.expired

2. Store Secrets in Vault

3. Enable and Restart


Operations

Update Deployment

Important: Always run cmd/migrate before restarting services after a code update. Migrations are idempotent.

NATS TLS Certificate Renewal

NATS TLS certificates (Let’s Encrypt, 90-day validity) renew automatically via the certbot systemd timer, which is overridden to use the Vault-backed wrapper script. Automatic renewal: The certbot systemd timer runs twice daily and calls /opt/hoodcloud/scripts/certbot-renew.sh. The wrapper authenticates to Vault via AppRole, fetches the Cloudflare API token, and runs certbot renew. If a renewal occurs, the deploy hook copies certs and sends SIGHUP to NATS (zero-downtime reload). Manual force renewal (testing):
Verify cert dates:

Payment mTLS Certificate Renewal

Payment mTLS certificates expire after 90 days:

Troubleshooting

Payment service: “permission denied”

Payment service: NATS “authorization violation”

Verify NATS JWT credentials are configured correctly:
  1. NATS_CTRL_ACCOUNT_PUB must match the CTRL account public key from nats-jwt-setup
  2. The signing seed in Vault (nats_ctrl_signing_seed) must be valid
  3. NATS server must be running in JWT operator mode with the correct operator/account JWTs

mTLS handshake fails

Compare CA fingerprints on both servers:

Temporal workflows stuck

See Vault Troubleshooting.