First deploy
Add a remote, write a manifest, run apply, watch TLS come up.
1. Register a remote
A remote is an SSH-reachable host that runs the voodu controller. The CLI talks to it by alias, not by IP.
voodu remote add voodu user@1.2.3.4This saves the SSH alias voodu (in your repo's .git/config, the same place git remote lives).
Using a specific SSH key — the :identity suffix
Append :<path-to-pem> to embed an SSH key path into the remote. The CLI then runs ssh -i <path> automatically on every command — no VOODU_SSH_IDENTITY env, no --identity flag per call.
voodu remote add voodu ubuntu@1.2.3.4 # uses ssh-agent / ~/.ssh/id_*
voodu remote add staging ubuntu@ec2.example.com:~/.ssh/staging.pem # embedded key
voodu remote add prod ubuntu@ec2.example.com:/etc/voodu/keys/prod.pemRules for the suffix:
- Must start with
/,~,./, or../(so it parses unambiguously as a path). ~expands to your home dir at parse time.- Bare tokens are rejected — voodu refuses to guess.
You can mix: one remote uses the agent, another uses a PEM. Each is independent.
The default-remote convention
The alias named voodu is the implicit default. When you run voodu apply without -r, the CLI uses it. That's it — no flag, no env var, just voodu apply.
For everything else (staging, prod, regional pairs), add more remotes and reach them with -r <alias>:
voodu remote add voodu ubuntu@1.2.3.4 # default (beta / dev)
voodu remote add staging ubuntu@5.6.7.8
voodu remote add prod ubuntu@9.10.11.12Then:
voodu apply -f web # → voodu (default)
voodu apply -f web -r staging # → staging
voodu apply -f web -r prod # → prodThis is the same shape git uses — origin is the default, everything else is explicit. Treat voodu as the "I just want to push the thing" host.
List what's registered:
voodu remote list2. Write web.hcl
app "prod" "web" {
image = "ghcr.io/myorg/demo:latest"
replicas = 2
env = {
PORT = "8080"
NODE_ENV = "production"
}
health_check = "/healthz"
host = "demo.example.com"
tls {
email = "ops@example.com"
}
}Two labels — "prod" (scope) and "web" (name) — uniquely identify the resource. That key flows through diff, apply, and prune.
3. Preview the plan
Against the default remote:
voodu diff -f webAgainst a named remote:
voodu diff -f web -r staging+ app/prod/web replicas=2 image=ghcr.io/myorg/demo:latest
+ ingress/prod/web host=demo.example.com tls.email=ops@example.com+ = create, ~ = modify, - = prune. Wire --detailed-exitcode in CI — exit 2 means changes pending.
4. Apply
Default remote:
voodu apply -f web→ packing context (1.4 MB)
→ streaming over ssh ubuntu@1.2.3.4
→ controller: planning ...
→ build → swap current → reconcile caddy
✓ apply complete in 11.8s
✓ https://demo.example.com · 2/2 healthyDifferent env:
voodu apply -f web -r staging
voodu apply -f web -r prodWhat just happened
- The CLI tarred your working directory and streamed it over SSH to
voodu receive-packon the selected remote. - The controller diffed the manifest against its embedded etcd, built the image (or skipped the rebuild if the tarball hash matched a previous one), and rolled the new replicas in.
- Caddy got a new route + an ACME cert request. TLS came up on the first valid challenge.
Watch it
voodu logs prod/web -f # tail the running container
voodu describe app/prod/web # manifest + status + pod list
voodu pods -s prod # all pods in scope "prod"Add -r staging / -r prod to inspect those remotes instead.
Roll back
voodu rollback app/prod/web # repoint `current` to the previous releaseReleases are content-addressed — rolling back is just a symlink swap, not a rebuild.
See also
voodu remote— full CLI surface for add / list / remove- Manifest overview —
(kind, scope, name), blocks, references