Skip to content

Deploy with GitHub Actions

This guide configures the prod environment so GitHub Actions runs deploy and verify—not your Mac. Use it when you want CI-driven lifecycle and do not need a permanent Mac WireGuard peer at node_controller_address. CI uses the Deploy workflow, not task up.

Host Role
static-1 Ubuntu 26.04 public endpoint (mesh hub, x86_64)
static-2 Second Ubuntu 26.04 public endpoint (x86_64)

Add a home-lab roaming node later with Adding a roaming node—after statics are up.

The same prod inventory works from your Mac or from GitHub Actions—the control plane differs.

Both paths use the same prod inventory shape. They differ by control plane.

Mac GitHub Actions
Guide Deploy to production This guide
Entry task up ENV=prod Actions → Deploy workflow
Who runs Ansible Your Mac GitHub-hosted runner
Permanent mesh peer Mac at node_controller_address (usually .1) None
During a job Ephemeral runner at node_ci_address (e.g. .254)
Lima guests dev only Not supported

Keep dev for local Mac development. Use prod for production—either from your Mac or from GitHub Actions, not both at once on the same inventory.

Keep a backup of inventories/prod/.vault-pass and the encrypted vault off-repo.

Read project from inventories/prod/hosts.ymlall.vars.project (currently example). In examples below, <project> is that value.

Replace template DNS names, public IPs, and SSH fingerprints for both static nodes.

Edit inventories/prod/hosts.yml the same way as Deploy to production — Step 1: set hostname, then fill public_ip and ssh_ed25519_sha256 for static-1 and static-2. Install this public key on each VM with static IP (ops user)—same prep as Adding a public node. Go and do that first.

Create records for the hostname you set in Step 1. Supercompute derives the rest by adding the hostname as a common suffix. Leave example.com at your registrar. If the DNS is hosted on Cloudflare, do not enable the proxy orange icons.

Prefix Name Type Value
ns ns.example.com A static-1 public IPv4
ns ns.example.com A static-2 public IPv4
sc-api sc-api.example.com CNAME ns.example.com
sc-app sc-app.example.com CNAME ns.example.com

Later, dynamic app URLs use the same nameserver:

Prefix Name Type Value
apps apps.example.com NS ns.example.com

Supercompute’s Nameserver then serves *.apps.example.com. If the VM IPs are not ready yet, add the A records before bring-up.

To use different API or app hostnames, edit dns_prefix_api and dns_prefix_app (and other dns_prefix_* values if needed) in inventories/<env>/group_vars/all/main.yml, then create matching DNS records and re-run task up.

Tell the inventory that GitHub Actions—not your Mac—will run deploys.

Edit inventories/prod/group_vars/all/main.yml and set:

control_plane: gha

Mesh CIDR (10.217.79.0/24), node_ci_address (.254), Ubuntu 26.04 amd64 image (template:ubuntu-26.04), and production resources are already set in the committed prod inventory. node_include_controller_peer becomes false automatically when control_plane: gha.

Install this public key on both production VMs (ops user)—follow Adding a public node for each host.

Use an empty passphrase—the runner cannot unlock a passphrase-protected key. The private key is GitHub secret OPS_SSH_PRIVATE_KEY later.

Install Ubuntu on each VM, create the ops user, and open bootstrap firewall ports.

For static-1 and static-2:

  1. Ubuntu 26.04 amd64 + public IP.
  2. Create ops, install ~/.ssh/example-prod.pub, passwordless sudo.
  3. Record each host’s SHA256:… from /etc/ssh/ssh_host_ed25519_key.pub.
  4. Fill public_ip and ssh_ed25519_sha256 in hosts.yml.
  5. Firewall: TCP 22 open for bootstrap (runner egress IPs change; plan accordingly). Open UDP 51830 broadly if you add roaming nodes later.

Prove SSH from your Mac before the first workflow run:

Terminal window
ssh -i ~/.ssh/example-prod -o IdentitiesOnly=yes [email protected] true
ssh -i ~/.ssh/example-prod -o IdentitiesOnly=yes [email protected] true

Create the encrypted vault locally and back up the password for CI.

Initialize vault locally—even though deploys run in CI, you create secrets from a trusted machine:

Terminal window
task vault-init ENV=prod

Creates inventories/prod/.vault-pass and encrypted vault.yml with vault_meta, placeholder vault_database_url, and auto-generated vault_database_secret. The Deploy workflow’s ensure-secrets step adds WireGuard keypairs on first up.

Decrypted inventories/prod/group_vars/all/vault.yml looks like this:

---
vault_meta:
project: example
provider: prod
vault_database_url: postgresql://REPLACE_WITH_USER:PASSWORD@HOST:5432/DATABASE
vault_database_secret: mT4nQ8pL2vK6wB9xF1hJ5cY0aZ3dE7gH_rXv

Keep inventories/prod/.vault-pass out of git (store the same value in GitHub secret ANSIBLE_VAULT_PASSWORD).

Point the vault at your external Postgres before committing it for CI.

Supercompute needs a Postgres database with owner role hosted outside of the Supercompute cloud.

Terminal window
task vault-edit ENV=prod

task vault-edit decrypts inventories/prod/group_vars/all/vault.yml in your editor. You should see vault_meta, vault_database_url, and vault_database_secret (WireGuard keys appear after the first task up). Replace only vault_database_url with your connection URI (same shape as the prod tab). Leave everything else unchanged.

Example after you set the database URL:

---
vault_meta:
project: example
provider: prod
vault_database_url: postgresql://app:[email protected]:5432/supercompute
vault_database_secret: mT4nQ8pL2vK6wB9xF1hJ5cY0aZ3dE7gH_rXv

Save and exit the editor:

  • vim/vi (default on macOS): press EscapeEscape, type :wq, press EnterEnter
  • nano: press Control + OControl + O, EnterEnter, then Control + XControl + X

Deploy workflow validation rejects the vault-init placeholder in vault_database_url.

Commit the encrypted vault; CI decrypts with ANSIBLE_VAULT_PASSWORD (no separate database URL GitHub secret).

Commit inventories/prod/hosts.yml, inventories/prod/group_vars/all/main.yml (with control_plane: gha), and encrypted inventories/prod/group_vars/all/vault.yml in the repository where Actions runs. Never commit .vault-pass.

Add the vault password and operator SSH key to GitHub Actions secrets.

In GitHub → SettingsSecrets and variablesActions, add:

Secret Required Value
ANSIBLE_VAULT_PASSWORD yes Exact contents of inventories/prod/.vault-pass
OPS_SSH_PRIVATE_KEY yes Full private key file (same as ~/.ssh/example-prod; must be passphrase-less)
MAC_OPERATOR_SSH_PUBLIC_KEY no Your Mac Ed25519 public key; if set, installed into ops authorized_keys on nodes

Every node must allow passwordless sudo for ops (the runner cannot answer a become password).

Trigger the Deploy workflow to bring up production from CI.

  1. GitHub → ActionsDeployRun workflow.
  2. env: prod.
  3. action: up, verify, or down.
  4. For down, set confirm to exactly down-prod.

What up does on the runner:

  1. Sync known_hosts from inventory fingerprints.
  2. Ensure vault secrets (WireGuard keypairs + vault_database_secret if missing).
  3. Validate hosts.yml (hostname) and vault (vault_database_url, vault_database_secret).
  4. Run mesh reconcile (wireguard-up) — no Mac LaunchDaemon.
  5. Briefly join the runner to the mesh at node_ci_address, install cluster software on nodes, then remove the runner peer.

The runner VM is discarded after the job; your static nodes stay up.

Confirm the workflow succeeded and nodes are reachable.

Re-run the workflow with action verify, or after an up job completes, SSH from your Mac if you installed MAC_OPERATOR_SSH_PUBLIC_KEY:

Terminal window
task ssh ENV=prod NODE=static-1

(Only works once your Mac can reach the mesh—typically after a successful up and if you rely on public bootstrap or an operator key on nodes.)

  • Do not run task up ENV=prod on your Mac after setting control_plane: gha.
  • Do not run the Deploy workflow against dev (control_plane: mac) unless you understand it skips the Mac peer.
  • Pick one control plane for prod: Mac (Deploy to production) or GHA (this guide), not both.
Item Done
control_plane: gha set in inventories/prod/group_vars/all/main.yml
hostname set in hosts.yml (for example example.com)
Parent DNS: ns A per static + api/app CNAME to ns + apps NS (Cloudflare proxy off)
static-1 + static-2 public IPs + fingerprints in hosts.yml
~/.ssh/example-prod created (empty passphrase); public key on both statics
ssh … true works for both static IPs
vault_database_url set in encrypted vault.yml; vault_database_secret from vault-init
GitHub secrets ANSIBLE_VAULT_PASSWORD + OPS_SSH_PRIVATE_KEY set
Workflow up completed successfully

Store off-repo: ~/.ssh/example-prod (+ .pub), inventories/prod/.vault-pass, and a copy of inventories/prod/group_vars/all/vault.yml.