docs: cloud deployment IaC design (OpenTofu + cloud-init + Hetzner) - #138
Conversation
Introduces a new container variant (Dockerfile.harvester) that bundles log-harvester (unytco/log-harvester) instead of log-sender. The harvester reads from log-collector, aggregates usage data, and parks invoices on Unyt Agreements via an embedded Holochain conductor. Key changes: - Dockerfile.harvester: single-stage wolfi-base build, clones and builds log-harvester from source via GITHUB_TOKEN build secret, bakes in unyt.happ from latest unytco/unyt-sandbox release (pinnable via UNYT_HAPP_VERSION build arg), exposes ports 4444 and 4445 - s6-overlay-harvester/: self-contained s6 service tree (conductor, log-harvester, logrotate-cron, setup) with no dependency on the base s6-overlay directory - log-harvester s6 service: waits for conductor readiness, installs unyt.happ, attaches app websocket on 4445, init/refreshes harvester config, runs harvester in loop mode - CI: build-and-push-harvester-image job publishing ghcr.io/holo-host/edgenode-harvester using HARVESTER_REPO_TOKEN secret - docker-compose.yml: edgenode-harvester service for local testing - Docs: LOG_HARVESTER_QUICKSTART.md, updated README.md, docker/README.md, docker/CHANGELOG.md Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds BATS test files and a test runner for the harvester container variant: - harvester_startup.bats: verifies conductor start, unyt.happ install, app-ws on 4445, config initialization, and log-harvester service start - harvester_process.bats: verifies holochain and node run as nonroot - run_harvester_tests.sh: runner analogous to run_tests_multi.sh, builds the harvester image with GITHUB_TOKEN secret and waits for full startup - pr-checks.yml: adds test-harvester-image CI job running on docker/** PRs Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds harvester_e2e.bats which submits real signed metrics via log-sender, verifies they reach D1, then runs the harvester (--today --dry-run) and asserts "fetched metrics count" > 0 and "Successfully invoiced logs." Trims harvester_integration.bats to two lightweight connectivity checks. Extends run_harvester_tests.sh to start edgenode and wait for its Holochain conductor before running the e2e test. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- CONFIG_PATH: /etc/log-harvester → /data/log-harvester (volume-mounted, survives restarts; drops now-redundant holo-config-harvester volume) - s6 run scripts: shebang → #!/command/with-contenv so HC_* env vars are available inside the service - log-harvester/run: remove add-app-ws call (handled by harvester init); add chown after both init and refresh paths - Dockerfile.harvester: GITHUB_TOKEN secret now optional — falls back to unauthenticated clone when git credentials already grant access - pr-checks.yml: switch to docker/build-push-action with GHA layer cache for edgenode, edgenode-harvester, and log-collector builds - run_tests_multi.sh: build log-collector separately before compose up to preserve layer cache across repeat runs - harvester_startup.bats: update config path and websocket test to match Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The docker/log-collector directory is gitignored (it's a separate repo). Add an actions/checkout step to clone it into place before the build. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The log-collector Dockerfile and entrypoint are ours, not upstream's. Store as Dockerfile.log-collector in the docker/ directory so CI can find them after checking out unytco/log-collector as the build context. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
secret-envs is not a valid build-push-action parameter; the correct field is secrets which mounts the value directly as a build secret. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
build-push-action secrets: does not reliably mount the github_token secret into the BuildKit RUN --mount. Revert to an explicit docker buildx build run: step with --secret id=github_token,env=GITHUB_TOKEN, which is the known-working approach. GHA layer cache flags are retained. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…rets Docker BuildKit secret mounting is unreliable in GHA. Mirror the log-collector approach: checkout unytco/log-harvester into docker/log-harvester-src/ before the build, then COPY it in. - Dockerfile.harvester: replace RUN --mount=type=secret git clone with COPY log-harvester-src - pr-checks.yml: add actions/checkout step for log-harvester - run_harvester_tests.sh: auto-clone log-harvester-src if absent - .gitignore: add docker/log-harvester-src/ Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
docker compose up -d was starting all services including edgenode-harvester, which requires log-harvester-src in the build context. Explicitly name only the services needed for the edgenode test suite. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
In CI the wrangler state directory is empty (no persistent local volume), so drone_registrations and other tables don't exist. Run wrangler d1 execute --local --file=schema.sql in the entrypoint before starting wrangler dev. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Wrangler dev reads vars from wrangler.toml, not Docker env vars.
Pass --var ADMIN_SECRET:${ADMIN_SECRET} so the worker picks up the
secret configured in docker-compose.yml.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
run_tests_multi.sh was running all *.bats files including harvester_*, which fail when edgenode-harvester isn't started. Exclude harvester files explicitly — they belong to run_harvester_tests.sh. Also add setup() skip guards to harvester_startup.bats and harvester_process.bats which were missing them. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add restart: unless-stopped to log-collector service so Docker auto-recovers when wrangler dev crashes under concurrent D1 load - Tighten healthcheck (15s interval, 5 retries, 30s start_period) - Run each .bats file separately in run_tests_multi.sh with a log-collector health-check wait between files, so a wrangler crash in integration_data_pipeline.bats doesn't cascade to later files Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- CHANGELOG: add all Unreleased entries for harvester variant work (Dockerfile.log-collector, run_harvester_tests.sh, BATS files, test runner isolation, restart policy) - TESTING: note per-file test isolation and harvester_* naming convention - LOG_HARVESTER_QUICKSTART: remove stale --secret flag from local build snippet; replace with git clone of log-harvester-src (required for COPY) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Dockerfile was renamed/consolidated; latest-unyt tag is not published. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds deploy/DESIGN.md describing a full-stack IaC approach for deploying Holochain hApps using Edge Node on Hetzner Cloud with Cloudflare for the joining service, UI, and log-collector. Covers staging/production workflow, env-var-based secrets pattern, tooling rationale, and operator adaptation guide. Includes deploy/.env.example as the secrets template. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Bundle h2hc-linker and Caddy into the standard edgenode image as toggleable s6-overlay services, eliminating the need for cloudflared or separate systemd services on the VM - Caddy provides TLS termination directly via Let's Encrypt, giving the linker a stable public URL (no tunnel, no seed-kv on restart) - harvester variant explicitly excludes linker and caddy - Generalise the CONDUCTOR_MODE toggle pattern to all services - Drop Ansible roles for linker and tunnel (now managed via container env vars); simplify to edgenode and harvester roles only - Add CLOUDFLARE_ZONE_ID and CADDY_DOMAIN to env var table - Resolve open questions on cloudflared (dropped) and tunnel stability Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
S3-compatible with endpoint override, keeps all infrastructure costs on Hetzner rather than splitting across providers. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Minimal build + OpenTofu config to answer three questions before committing to full IaC implementation: 1. Can cloudflare_worker_script handle esbuild-bundled TypeScript Workers? 2. Can it resolve the joining service's cross-directory imports? 3. Do KV (joining service) and D1 (log-collector) bindings work via the provider? See deploy/spike/README.md for steps and success criteria. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Use NODE_PATH env var instead of invalid --node-paths flag - Add --external:node:* (CF nodejs_compat handles these at runtime) - Add --external:@holo-host/lair (membrane proof, not needed for spike) - Add --external:buffer (dynamic require in @bitgo/blake2b-wasm) - Make MEWSFEED_DIR and JOINING_SERVICE_DIR configurable env vars; joining-service is a sibling repo, not inside mewsfeed - Both bundles now build: joining.js (149kb), log-collector.js (1.2mb) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces deprecated cloudflare_worker_script with the current three-resource pattern: cloudflare_worker + cloudflare_worker_version + cloudflare_workers_deployment. Updates bindings to use the unified bindings array with type field (kv_namespace, d1, plain_text, secret_text) instead of separate binding blocks. Uses content_file to avoid bloating state with large bundle content. Source: https://developers.cloudflare.com/workers/platform/infrastructure-as-code/ Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Upgrade Cloudflare provider to v5.x (v4.x lacks cloudflare_worker, cloudflare_worker_version, cloudflare_workers_deployment) - Add nodejs_compat compatibility flag to joining worker version - Bundle @holo-host/lair instead of externalising it; build lair from the holo-web-conductor workspace before running esbuild - Add read_replication and subdomain fields to resolve provider drift - Resolve DESIGN.md open question 2: log-collector deploys via OpenTofu; joining service stays on wrangler deploy due to libsodium WASM issue - Add Appendix A with full spike findings and options for @holo-host/lair Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
zippy
left a comment
There was a problem hiding this comment.
Reading over the design doc it seem pretty reasonable, there's one problem, and I just have one questions:
Problem: it looks like the design show the log-harverster running on a node by itself which it can't. It needs a conductor running on the same node to which it connects to actually create the invoices. It does so via the js @holochain/client node-js package. So one of the deployment issues is how that node is authenticated to the network via the joining-services CLI interface, and also how it is signing it's zome-calls. For us this hasn't been decided I don't think, it could happen a few different ways.
Question: is this the right time to be adding ansible into the mix? It seems like it might be an extra layer that isn't actually needed right now because we could just be using a simple cloud-init file for deployment on Hetzner. On the other hand, it may be right to add it from the start. Just want to confirm the thinking.
FYI the harvester edgenode actually runs conductor + Unyt hApp + log-harvester. The doc is mistaken and will be corrected. |
- Add Data Persistence section: Hetzner persistent volumes as a hard requirement for lair keystore and conductor database survival - Replace Ansible with cloud-init + shell scripts; document rationale and deferral of Ansible - Fix harvester description: runs conductor (Unyt hApp) + log-harvester, not log-harvester only - Add Disaster Recovery section: backup strategy (Hetzner volume snapshots), three recovery scenarios, RTO/RPO objectives - Clarify architecture: single joining service shared by hApp and Unyt, single harvester per deployment, 2 edgenodes for staging - Resolve open questions: VM count, VM sizing, state backend, bootstrap - Add open question: platform-track self-registering harvester - Replace Makefile with scripts/ directory; remove make from workflow - Fix open question numbering Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Documents the joining service admin API approach for automated harvester bootstrap at platform scale, including endpoint design, KV-backed agent store, cloud-init integration, and scope of changes required. Option A (manual bootstrap script) remains in place for the staging PoC. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove Ansible section (SSH_KEY_PATH → SSH_PUBLIC_KEY) - Add CLOUDFLARE_ZONE_ID and SSH_PUBLIC_KEY variables - Add HARVESTER_LAIR_PASSWORD for harvester conductor lair keystore - Complete TF_VAR passthrough for all new variables - Update source instructions to reference deploy scripts (not make/ansible) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
Thanks for the review @zippy — both concerns are addressed in the current branch HEAD: Conductor requirement: The design already reflects this correctly. The harvester VM table shows Ansible: The design explicitly defers Ansible in favour of cloud-init — there's a 'Why not Ansible (for now)' section covering the reasoning. For the current scope (a small number of VMs each running one container) cloud-init is sufficient and keeps the toolchain simpler. Ansible is noted as a candidate if operational needs grow (rolling updates across many nodes, config drift correction). The PR title and description have been updated to remove the Ansible reference. Let me know if anything else needs clarification. |
These three files were modified during the exploratory spike before PR #137 landed fixes. Reset to main: - Dockerfile.harvester: remove redundant mkdir -p /app - docker-compose.yml: remove dead build secrets block - run_harvester_tests.sh: restore GITHUB_TOKEN clone support and error handling Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds two new optional s6-overlay services to the standard edgenode image: - h2hc-linker: auto-enabled when H2HC_LINKER_ADMIN_SECRET is set; waits for the conductor admin port before starting; reverse-proxied by Caddy - caddy: auto-enabled when CADDY_DOMAIN is set (and H2HC_LINKER_ADMIN_SECRET is set); generates a Caddyfile at runtime and terminates TLS via Let's Encrypt for the linker's public endpoint Both services use exec tail -f /dev/null when their toggle variable is absent, keeping s6 happy without treating the disabled state as a crash. Dockerfile changes: - apk add caddy - arch-aware h2hc-linker download (LINKER_VERSION=0.1.0) - EXPOSE 80 443 added alongside existing 4444 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Tier 1 (always run in CI — no env vars needed): - h2hc-linker and caddy binaries are installed - neither process runs when H2HC_LINKER_ADMIN_SECRET / CADDY_DOMAIN unset - ports 80 and 443 are not bound in the disabled state Tier 2 (skipped unless services are configured): - h2hc-linker runs as nonroot and listens on H2HC_LINKER_PORT - caddy runs, listens on 443, generates /tmp/Caddyfile with the domain - startup.log contains expected messages for both services Tier 2 tests activate automatically when the services are running; run via a compose override that sets H2HC_LINKER_ADMIN_SECRET and CADDY_DOMAIN. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ames - Bump LINKER_VERSION 0.1.0 → 0.1.1 in Dockerfile - Fix asset names: x86_64-unknown-linux-gnu → linux-x86_64 style - Rewrite linker/run: use env vars only (no CLI flags), require H2HC_LINKER_BOOTSTRAP_URL to enable, default H2HC_LINKER_ADDRESS=0.0.0.0 - Fix caddy/run enable condition: H2HC_LINKER_ADMIN_SECRET → H2HC_LINKER_BOOTSTRAP_URL - Update DESIGN.md and linker_caddy.bats for new API Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add linker and caddy rows to the services table with auto-enable conditions - Add 'Optional services' subsection explaining the idle/toggle mechanism, env vars for both services, and example docker run commands Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Summary
Proposes an Infrastructure-as-Code approach for deploying Holochain hApps using Edge Node on Hetzner Cloud + Cloudflare. Covers the full stack: Cloudflare Pages/Workers (joining service + log-collector), Hetzner VMs (edgenode + harvester), h2hc-linker, Caddy TLS.
What's included
deploy/DESIGN.md— full design with architecture, DR, open questions, and spike findings (Appendix A)deploy/.env.example— secrets templatedeploy/spike/— working OpenTofu + esbuild spike answering Cloudflare Worker deployment questionsKey design decisions
@holochain/clientghcr.io/holo-host/edgenode-bootstrap); generates agent key, whitelists it in the joining service, installs Unyt hApp; agent key is stable for the lifetime of the persistent volumeSpike findings (Appendix A)
wrangler deploy—@holo-host/lair→ libsodium WASM can't be bundled with esbuild alone; wrangler handles it transparentlyOpen questions (unresolved)
@holo-host/lairlibsodium WASM fix — needed before joining service can move to OpenTofu deploymentTest plan
@holo-host/lairdeveloper🤖 Generated with Claude Code