Voodu iconvoodu
How it worksManifestStackDocsSSOEnterpriseFAQ
★ Star on GitHub
Voodu iconv0.18.15 · voodu apply, no git push

Self-hosted PaaS.
Commitless deploys.

Voodu is a Heroku-shaped, Kubernetes-honest deploy tool you run on your own boxes. One HCL file. One voodu apply. No git push, no bare repo, no plugin sprawl — apps, ingress with TLS, and stateful services that actually back themselves up.

★ Star on GitHubRead the docs ↗
Elastic License 2.0Linux · macOSGo · Caddy · Docker100% self-hosted
1 filevoodu.hcl is the source of truth
1 commandvoodu apply does build, ship, route, swap
0 git pushestarball over ssh, content-addressed
TLS by defaultCaddy + Let's Encrypt, on-demand wildcards
// the manifest

One file describes the running system.

HCL out-of-the-box. No YAML to coerce, no Compose tricks, no chart-of-charts. Apps, ingress, databases, jobs — every kind shares the same shape and the same blast radius.

voodu.hcl · HCL · 26 lines
// Production stack: HA postgres + HA redis behind the app.
app "myorg" "web" {
  image    = "ghcr.io/myorg/web:latest"
  replicas = 3
  host     = "app.example.com"

  env = {
    DATABASE_URL = "postgres://myorg/db"
    REDIS_URL    = "redis://myorg/cache"
  }
  tls { email = "ops@example.com" }
}

postgres "myorg" "db" {
  image    = "postgres:16"
  replicas = 3            // 1 primary + 2 standbys
  database = "myapp"
}

redis "myorg" "cache" {
  replicas = 3
}

redis "myorg" "cache-ha" {
  sentinel { monitor = "myorg/cache" }
}
Two labels

scope + name, every kind

Every resource is keyed by (scope, name). Scope groups apps, envs or teams; name is unique inside it. The same key flows through diff, apply, and prune.

Provision, don't orchestrate

Databases ship as first-class kinds

Postgres with replicas + nightly backup, Redis with Sentinel — declared in the same file as your app. Mongo, RabbitMQ, Kafka land via the plugin model; /opt/voodu/plugins is where new kinds appear.

Out of the box

TLS, health, ingress — no chart

Add tls{} and voodu cuts a Let's Encrypt cert via Caddy. host wires ingress. No Helm, no sidecars, no YAML blast radius.

Upsert-only by default

No accidental deletions

apply only creates and updates. Resources missing from the file stay put. --prune is the explicit opt-in, scoped per (scope, kind)so siblings of other types aren't touched.

// how it works

Laptop talks. Server reconciles.

The CLI streams your build context over SSH. The controller diffs it against embedded etcd, builds, swaps, and reroutes. It's k8s-shaped reconciliation without the k8s-shaped operations bill.

your laptop                                 your server
───────────                                 ───────────
$ voodu apply -f voodu.hcl  ──ssh──▶  voodu-controller
  │                                         │
  │                                         └─ reconcile ingress / services (etcd)
  │  (build mode only: stream tarball)
  └─ tar -czf - <path>     ──ssh──▶  voodu receive-pack
                                              └─ extract → build image
                                                 → swap `current` symlink
                                                 → run post_deploy hooks
                                                 → recreate container
01 — DECLARE

Write the manifest

One HCL file, or many. Apps, ingresses, databases, jobs — pick what you need, ignore the rest.

# voodu.hcl
app "prod" "api" { ... }
ingress "prod" "api" { ... }
02 — DIFF

See the plan first

voodu diff dry-runs the apply: what's new, changed, pruned. Wire --detailed-exitcode in CI to gate deploys.

$ voodu diff -f voodu.hcl
~ deployment/prod/api
  ~ replicas  1 → 3
+ redis/clowk-lp/redis-ha
03 — APPLY

Ship over SSH

Same flags, no surprises. Tarball is content-addressed — identical trees skip the rebuild and just repoint current.

$ voodu apply -f voodu.hcl -r prod-1
✓ apply complete in 11.8s
✓ 3/3 healthy
// cli

A small, k8s-shaped command surface.

Six verbs cover ~95% of day-to-day. Everything else is a plugin discovered from /opt/voodu/plugins.

Bootstrap a host
$voodu remote setup prod-1 ubuntu@host

SSH preflight + install + server-mode setup. Idempotent.

Create an app
$voodu apps create prod

Seeds /opt/voodu/apps/prod with a fresh .env.

Apply a manifest
$voodu apply -f voodu -r prod-1

Streams context over SSH, reconciles, swaps containers.

Preview drift
$voodu diff -f voodu.hcl --detailed-exitcode

Plan-style output. Exit 2 means changes pending.

Manage env
$voodu config set DATABASE_URL=… -a prod

Out-of-band secrets. Always wins over manifest env blocks.

Install a plugin
$voodu plugins:install thadeu/voodu-postgres

Drops a binary into /opt/voodu/plugins. Voodu wires it up.

// lifecycle

Declared. Observed. Operated.

The CLI that ships resources is the CLI that watches them. Postgres replicas, Redis Sentinel state, job histories — all visible from one place, without a second portal.

provision

Apply, and it exists.

$vd apply -f stack.hcl
packing web
building release (12s)
postgres/myorg/db applied
redis/myorg/cache applied
deployment/myorg/web applied
apply complete (3 resources)

Postgres replicas, Redis Sentinel, app behind TLS — every kind shipped by one apply. No portals, no chart-of-charts.

observe

See it. Get notified.

$vd stats myorg
NAME CPU MEMORY REPLICAS
myorg/web 45m 128Mi/512Mi 3/3
myorg/db 180m 512Mi/1Gi 3/3
myorg/cache 12m 64Mi/256Mi 3/3
 
# on_probe → Telegram on transition
🚨 myorg-db-2 liveness failed: HTTP 503

Live CPU + memory via vd stats. Per-probe push notifications via on_probe — Telegram, Slack, PagerDuty fire the moment a pod transitions. No agent, no Grafana, no portal in the middle.

operate

One CLI, every day.

$vd logs -f myorg/db --tail 2
[14:23:01] replica sync ok
[14:23:02] checkpoint complete
 
$vd rollback myorg/web --to v17
web rolled back to v17 (3 replicas)

Stream logs across replicas, rollback by release ID, exec into any container. vd is the only surface — no SSH gymnastics, no portal hand-offs.

See a production stack →
// stack

Built on boring, well-loved parts.

Voodu is opinionated where it counts and pluggable where you'd want it to be. Postgres, Mongo, Redis, ingress — independent binaries, not a rewritten universe.

core
core

Go controller

Single static binary, embedded etcd, HTTP API on 127.0.0.1:8686.

core

Caddy ingress

Replaces NGINX. Caddy Admin API, ACME, on-demand wildcard TLS.

core

Docker builds

Bring a Dockerfile or let voodu auto-detect Go, Ruby, Rails, Python, Node.

core

SSH transport

Tarball over SSH. No daemons listening on the public internet.

plugins
plugin

voodu-postgres

Postgres with backup, replica and test-restore. Declared like an app.

plugin

voodu-redis

Redis service with the same reliability primitives.

plugin

voodu-caddy

Reference plugin. Read it to write your own.

plugin

Build your own

Independent binaries discovered from /opt/voodu/plugins. Install via GitHub repo.

// faq

Questions devs actually ask.

Why HCL and not YAML?+
HCL is built for humans declaring infrastructure. Blocks, labels, and typed values map cleanly to kind/scope/name. Voodu is HCL-only — no YAML coercion, no indentation traps.
How is this different from Dokku, Coolify, CapRover?+
Those are excellent — and mostly git-push or click-driven. Voodu is declarative at the layer above: one file describes the system, diff shows the plan, apply reconciles. Stateful services are first-class, not bolted on as plugins-of-plugins.
Is it really commitless?+
Yes. voodu apply tars whatever directory you're in and streams it over SSH. No bare repo, no git push, no commit required. Edit, save, apply.
What about K8s?+
Voodu borrows the shape (declarative, reconciler, source-of-truth) and skips the operations bill (no CNI, no Helm chart, no kube-state-metrics dashboard). One controller, embedded etcd, done.
Does it scale beyond one host?+
Three prod boxes behind an ALB? Add three remotes and loop voodu apply -f voodu.hcl -r $r. Same manifest, different SSH targets. The controller is per-host and stateless about its peers — keep it boring.
How do secrets work?+
voodu config set stores env vars per-app, out-of-band from the manifest. config:set always wins over envblocks, so a runaway apply can't reset a production secret.
License?+
Elastic License 2.0. Read it, run it, modify it for your own use, self-host it for free — the one thing it stops is reselling voodu as a competing hosted product. No cloud tier, no usage telemetry.

Stop deploying from a Slack thread.

★ Star on GitHub

Elastic License 2.0 · v0.18.8 · made by @thadeu