Infrastructure-as-Code using OpenTofu for Hetzner Cloud deployment.
Manages Engram's production infrastructure on Hetzner Cloud with DNS via Vercel. All services run on a single Hetzner server with Caddy reverse proxy, deployed via Docker Compose.
- Server:
cpx31(4 vCPU, 8GB RAM, 80GB SSD) in Ashburn, VA - OS: Ubuntu 24.04 with Docker, Docker Compose v2, UFW firewall
- User:
engramwith passwordless sudo - Firewall: SSH (22), HTTP (80), HTTPS (443), ICMP
- apex: Points to Hetzner server IPv4
- api: API gateway (Caddy routes to backend services)
- observatory: Neural Observatory UI
- HTTP backend via Engram API (
/v1/tofuendpoint) - Requires API key with
state:writescope
cd packages/infra
# Set required environment variables
export TF_HTTP_USERNAME="tofu" TF_HTTP_PASSWORD="your-api-key"
export TF_VAR_domain="example.com" TF_VAR_hcloud_token="..." TF_VAR_vercel_api_token="..." TF_VAR_ssh_public_key="ssh-ed25519 ..."
# Initialize, plan, and apply
tofu init -backend-config="address=https://api.${TF_VAR_domain}/v1/tofu"
bun run plan && bun run up| Variable | Description |
|---|---|
hcloud_token |
Hetzner Cloud API token |
vercel_api_token |
Vercel API token for DNS |
ssh_public_key |
SSH public key content |
domain |
Base domain (e.g., example.com) |
engram_tuner_client_secret |
OAuth client secret for tuner service |
engram_search_client_secret |
OAuth client secret for search service |
engram_console_client_secret |
OAuth client secret for console service |
engram_ingestion_client_secret |
OAuth client secret for ingestion service |
Optional: vercel_team_id, server_name (default: engram), server_type (default: cpx31), location (default: ash)
bun run init # Initialize OpenTofu
bun run validate # Validate configuration
bun run fmt # Format .tf files
bun run plan # Preview changes
bun run up # Apply changes (auto-approve)
bun run down # Destroy infrastructure (auto-approve)
bun run output # Show outputs
bun run state # List state resources
bun run test # Run OpenTofu tests| Output | Description |
|---|---|
server_ip |
Public IPv4 address |
ssh_command |
SSH connection command |
api_url |
API gateway URL |
observatory_url |
Observatory UI URL |
oauth_*_client_id |
OAuth client IDs for each service |
oauth_auth_server_url |
OAuth authorization server URL |
oauth_env_* |
Sensitive OAuth environment variables (JSON) |
To retrieve OAuth environment variables for deployment:
# Get all OAuth env vars for a service (JSON format)
tofu output -json | jq -r '.oauth_env_tuner.value'
tofu output -json | jq -r '.oauth_env_search.value'
tofu output -json | jq -r '.oauth_env_console.value'
tofu output -json | jq -r '.oauth_env_ingestion.value'
# Export as shell environment variables
eval $(tofu output -json | jq -r '.oauth_env_tuner.value | to_entries | .[] | "export \(.key)=\(.value)"')Caddy handles TLS termination and reverse proxying:
api.{domain}→ API gateway with path-based routingapi.{domain}/v1/search→ Search serviceapi.{domain}/v1/tuner→ Tuner serviceobservatory.{domain}→ Observatory frontend
Services are deployed via docker-compose.prod.yml in /opt/engram on the server.
packages/infra/
├── backend.tf # HTTP state backend
├── dns.tf # Vercel DNS records
├── firewall.tf # Hetzner Cloud firewall
├── outputs.tf # Output values
├── providers.tf # Hetzner + Vercel providers
├── server.tf # Server + cloud-init
├── ssh.tf # SSH key resource
├── variables.tf # Input variables
├── versions.tf # Provider versions
└── tests/ # OpenTofu tests