Deploy and operate Rust, Go and other compiled web applications on your own Linux servers. Each site gets a domain, automatic HTTPS, its own PostgreSQL database, backups, health-checked updates and resource limits. Nothing to build and no runtime to maintain on the server: the app is a published binary, and Setu runs it as a hardened systemd service.
A real run: the installer, the panel, a Vyasa site provisioned in about ten seconds, live over HTTPS.
- One binary.
setuis the control plane, the per-server agent and the privileged helper, selected by subcommand. One static musl (x86_64 and aarch64) archive per release. - Native systemd, real limits. Every site runs as
setu-site@<id>.serviceunder its own user, with memory and CPU limits enforced by cgroups and a hardened unit. No container runtime in the path. - A small, typed root surface. The agent is not root. It reaches
root only through
setu helper <verb> --site <id>: typed JSON on stdin, no paths, modes or owners from the caller. The whole sudoers file is two lines. - Caddy in front. Automatic HTTPS through ACME (or Caddy's internal CA), programmed through its admin socket, one route per domain.
- Updates that roll back. An update backs up first; if the new version does not come up healthy, Setu puts the old binary and env back and, when the new version already ran, restores the database and data directories too.
You need a fresh Ubuntu 22.04 or 24.04, or Debian 12 or 13, server (x86_64 or aarch64), root, ports 80 and 443 open, and a DNS name pointing at the server. The installer brings PostgreSQL 16 or 17 depending on the distribution, and Caddy itself.
curl -fsSLO https://github.com/setucp/setu/releases/latest/download/install.sh && sudo bash install.sh --panel-host panel.example.comThe script verifies the release archive against SHA256SUMS, installs
/opt/setu/setu, creates the setu user and database, configures
Caddy for the panel host and starts setu.service. Then:
- It prints
https://panel.example.com/setupand a one-time setup token. Open the URL and paste the token. - The wizard creates the owner account (this works exactly once).
- The wizard registers this server: run the
setu agent install …line it shows, as root, with the join token inSETU_JOIN_TOKEN. The server showsonlinewithin seconds. - Apps → Add: paste or link an app manifest (see Apps).
- New site → app → domain: Setu provisions the site and it is live with TLS, its own database, limits and a nightly backup.
No DNS name yet? --tls internal uses Caddy's internal CA instead of
ACME. All flags, the manual install, upgrading and uninstalling are in
docs/INSTALL.md.
browser
│ HTTPS
┌─────────────────────▼──────────────────────────────┐
│ control-plane box │
│ Caddy ──unix socket──▶ setu serve │
│ REST API, web UI, jobs, │
│ scheduler ──▶ Postgres │
└─────────────────────▲──────────────────────────────┘
│ agents long-poll over HTTPS
│ (outbound only, per-server token)
┌─────────────────────┴──────────────────────────────┐
│ managed server (the same box, or any other) │
│ setu agent run ──sudo -n──▶ setu helper <verb> │
│ │ units, users, env, │
│ │ installs, backups │
│ ├─▶ Postgres (one database and role per site) │
│ ├─▶ Caddy admin socket (one route per domain) │
│ └─▶ setu-site@<id>.service (cgroup limits) │
│ nftables: site ports 20000-29999 not reachable │
│ from outside │
└────────────────────────────────────────────────────┘
The control plane is the source of truth and never SSHes; it queues typed, idempotent commands that agents long-poll for, so every retry is safe. Provisioning a site runs as a resumable job: validate → create user and dirs → create database → install binary → switch version → write unit → write env → start → health check → add Caddy route → verify → finish.
More: docs/AGENT.md (agents and the protocol), docs/OPERATIONS.md (backups, upgrades, token rotation), and the design spec.
An app is a TOML manifest per version. It is the only app-specific knowledge Setu has:
schema = 1
[app]
id = "hello"
name = "Hello"
version = "0.1.0"
homepage = "https://example.com"
health = "/healthz"
admin_path = "/"
[run]
kind = "binary"
args = ["serve"]
[run.binary."x86_64-linux"]
url = "https://example.com/hello-0.1.0-x86_64-unknown-linux-musl.tar.gz"
sha256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
exec = "hello-0.1.0/hello"
[needs]
postgres = ">=16"
dirs = ["notes"]
[env]
HELLO_BIND = { from = "bind_addr" }
DATABASE_URL = { from = "postgres_url" }
HELLO_NOTES_DIR = { from = "dir", dir = "notes" }
HELLO_TOKEN = { generate = "hex32", secret = true }Shortened: a complete manifest also needs the [limits.default],
[backup] and [update] tables, as in the full hello and Vyasa
examples.
The contract for app authors: ship one static binary in a tarball,
read the address to bind and everything else from environment variables
the manifest declares, answer 2xx on the health path when ready, and
keep persistent state only in the declared directories and the
database. Field reference, env rules and examples:
docs/MANIFEST.md.
In: one server or a small fleet, the native binary runtime, the web UI
and REST API (OpenAPI at /api/v1/openapi.json), operators with roles,
sealed secrets, local backups with restore, updates with rollback,
resource limits, Caddy TLS.
Not yet, by design:
- Containers as a runtime (phase 2; the manifest already carries
run.kind = "image"). - Site moves and server drain, off-box (S3) backups, enforced disk quotas (disk is reported, not enforced).
- Billing and customers (phase 3).
- aarch64 builds, a container image of Setu itself, TOTP, operator API tokens and a CLI client.
Managed interpreted runtimes (PHP, Node, Python) are out of scope; apps that need an interpreter will ship as an image. The plan for later phases is in docs/ROADMAP.md.
Start with docs/TRUST.md, the trust model in plain words.
The control plane is effectively root on every server that joins it:
agents carry out the commands it queues, some through the privileged
helper. The helper bounds what root can be asked to do (typed verbs,
fixed templates, every path derived from the site id, hostile archives
refused) but not who asks, so protect the control plane, its database
and its secret_key accordingly. Sites are untrusted: each runs as its
own user and can write only its data/ and run/ directories. Sites
on one server share the kernel, the local Postgres instance (one
database and role each) and Caddy. Secrets are sealed at rest, never
put on a command line or in logs, and revealed only with the operator's
password. The full model, the helper's verbs, the sudoers file and the
firewall rules are in docs/SECURITY.md.
To report a vulnerability, please contact the maintainers privately rather than opening a public issue.
Requirements: the Rust toolchain pinned in rust-toolchain.toml,
Docker (for the throwaway test Postgres and the end-to-end tests),
Node with pnpm (for the UI).
# the gate, before every commit
cargo fmt --check
cargo clippy --workspace --all-targets --all-features -- -D warnings
bash scripts/test.sh # starts a throwaway Postgres container on port 55440
# the web UI (React, embedded into the binary at build time)
pnpm -C ui install
pnpm -C ui lint && pnpm -C ui typecheck && pnpm -C ui test
bash scripts/build-ui.sh # production bundle for `cargo build`
# end-to-end proofs (see docs/E2E.md)
SETU_E2E_DIR=/tmp/setu-e2e bash scripts/e2e.sh # 14 assertions in a systemd container
SETU_INSTALL_DIR=/tmp/setu-install bash scripts/test-install.sh # the installer, 9 assertions
bash scripts/check-links.sh # relative links in the docsscripts/test.sh refuses port 5432; tests never touch a real
database. Migrations are forward-only; after changing a query,
regenerate .sqlx/ with scripts/sqlx-prepare.sh. UI API types come
from the checked-in openapi.json (pnpm -C ui gen:api).
| Document | What it covers |
|---|---|
| docs/INSTALL.md | install.sh and its flags, the setup wizard, manual install, upgrading, uninstalling |
| docs/OPERATIONS.md | scheduler and backups, control-plane backup and restore, upgrades, TLS mode, token rotation, rebuilding a server |
| docs/MANIFEST.md | the app manifest: fields, env rules, health, examples |
| docs/AGENT.md | setu agent install, doctor, state and reconcile, the agent protocol |
| docs/SECURITY.md | trust model, the privileged helper, network, secrets at rest |
| docs/E2E.md | the end-to-end and installer tests |
| docs/RELEASING.md | cutting a release |
| docs/ROADMAP.md | phases 2 to 4 (containers, billing, catalogue) and later |
| docs/specs/2026-10-09-setu-design.md | the design spec, as built |
| docs/brand/README.md | logo, mark and colours |
| CHANGELOG.md | release notes |
Setu 0.1.3 supports Ubuntu 22.04, 24.04 and 26.04 and Debian 12 and 13, on
x86_64 and aarch64 (see the support matrix in docs/INSTALL.md); other
distributions are refused by the installer unless --force-distro is
given. Expect rough edges and breaking changes before
1.0.
Bug reports, questions and app manifests are welcome as GitHub issues. For anything larger than a fix, please open an issue to discuss it before sending a pull request, and run the gate above before you do.


