voodu apply
Stream context, plan, reconcile.
Synopsis
voodu apply -f voodu -r prod-1 [flags]What it does
- Tar the working directory (build mode) or skip (image mode).
- Stream the tarball over SSH to
voodu receive-packon the remote. - Controller diffs the manifest against embedded etcd.
- Builds the image (or skips — tarball hash is content-addressed).
- Swaps
currentsymlink, runsreleasehooks, recreates containers. - Reconciles ingress and Caddy routes.
Flags
| Flag | Default | Description |
|---|---|---|
-f, --file | — | Manifest file or directory. Repeat for multiple. |
-r, --remote | — | SSH alias from voodu remote list. |
--prune | false | Opt-in. Delete resources in the same (scope, kind) that aren't declared in this apply. Default is upsert-only — siblings stay put. |
-y, --auto-approve | false | Skip the interactive confirmation prompt (also VOODU_AUTO_APPROVE=1). |
--force | false | Skip the up-to-date checks: rebuild build-mode deployments on a tarball-hash hit and re-pull every registry-mode image (also VOODU_FORCE_REBUILD=1). See Forcing a redeploy. |
-v, --verbose | false | Show raw docker build output instead of collapsing to a spinner. |
Examples
# Standard apply
voodu apply -f voodu -r prod-1
# Multi-file
voodu apply -f infra/web.voodu -f infra/redis.voodu -r prod-1
# Multi-remote loop
for r in prod-1 prod-2 prod-3; do
voodu apply -f voodu.hcl -r $r
done
# CI gate
voodu diff -f voodu.hcl --detailed-exitcode -r prod-1
# exit 2 = changes pending, exit 0 = no-op
# Opt-in cleanup of stale siblings (full source-of-truth apply)
voodu apply -f voodu.hcl --prune -r prod-1
# Re-pull a mutable tag CI just overwrote (ECR, GHCR, :latest)
voodu apply -f voodu.hcl --force -r prod-1Forcing a redeploy
Apply is up-to-date-aware in two places, and --force skips both.
Build mode ships the working directory as a content-addressed
tarball. An identical tree skips the rebuild and just repoints the
current symlink. --force rebuilds anyway — useful for a
non-deterministic build cache, or to validate a CI image change.
Registry mode names an image the host pulls. Docker only fetches a
tag it doesn't already have locally, so a mutable tag — :latest,
:main, the ECR tag your pipeline overwrites on every push — keeps
resolving to the digest cached on first deploy. The manifest never
changes, the diff comes back empty, and the old container keeps
running. --force makes the controller docker pull every image the
manifests name before reconciling:
voodu apply -f voodu.hcl --force -r prod-1
✓ pulling images (--force) (12s)
✓ pulled 123456789012.dkr.ecr.us-east-1.amazonaws.com/api:latest
✓ deployment/prod/api appliedWhat follows the pull is the normal reconcile. If the tag moved, the
controller sees the running container on a different image ID than the
tag now resolves to and rolls the replicas one at a time. If the tag
was already current you get already up to date and nothing
restarts — that is the correct outcome, not a swallowed apply. To
restart regardless of the image, use voodu restart <scope>/<name>.
--force also bypasses the No changes. Nothing to apply.
short-circuit, since a byte-identical manifest is exactly the shape
that needs the pull.
A pull that fails aborts the apply before anything is written — desired state is left untouched so a retry after fixing the registry credentials is a clean re-apply. The one exception: when the image is already on the host, the failure degrades to a warning and the apply proceeds with the local copy.
Private registries authenticate through the entries a
registry manifest writes, the same ones
the container-create path uses. For ECR on an EC2 instance role there
is no token to manage at all — see
Private registry → ECR with an EC2 instance role
for the whole flow, --force included.
Variable interpolation
${VAR} and ${VAR:-default} resolve on your machine before the tarball ships. Two sources:
- Your shell env (
os.Environ()). env_from'd config buckets — when a resource declaresenv_from = ["scope/name"], the CLI fetches that bucket from the controller before parsing. So${SLACK_WEBHOOK_URL}inon_deploy.success.urlcan come fromvoodu config set -s prod -n shared SLACK_WEBHOOK_URL=.... No per-devexportneeded.
Shell wins over bucket on collision — ad-hoc override works for testing without touching the bucket.
deployment "prod" "api" {
env_from = ["prod/shared"]
on_deploy {
success { url = "${SLACK_WEBHOOK_URL}" } # resolved from prod/shared
}
}Caveat: bucket-fed interpolation is local-apply only. With -r <remote> the SSH-forward path keeps shell-only.
Output
→ packing context (1.4 MB)
→ streaming over ssh ubuntu@prod-1
→ controller: planning ...
+ app/prod/api replicas=3 image=ghcr.io/myorg/api:1.7
~ ingress/prod/api tls.email=ops@example.com
- cronjob/prod/old-cleanup
→ build → swap current → reconcile caddy
✓ apply complete in 11.8s
✓ https://api.example.com · 3/3 healthyFrom CI
The same command runs from a GitHub workflow through clowk-in/voodu-gh — it installs the CLI, prepares SSH, and calls vd apply with --auto-approve. See GitHub Actions for the wiring and for what the server does with the result.