diff options
| author | Claude <noreply@anthropic.com> | 2026-08-05 23:14:29 -0500 |
|---|---|---|
| committer | Claude <noreply@anthropic.com> | 2026-08-05 23:14:29 -0500 |
| commit | 9f7c9f2f31acd7bc544dd988af1c8c2fe63ec504 (patch) | |
| tree | 6d7ab2a965c3e138a56c625197658ef2b324b045 /README.md | |
Initial commit of skiff scaffolding
Diffstat (limited to 'README.md')
| -rw-r--r-- | README.md | 168 |
1 files changed, 168 insertions, 0 deletions
diff --git a/README.md b/README.md new file mode 100644 index 0000000..632fbb0 --- /dev/null +++ b/README.md @@ -0,0 +1,168 @@ +# Skiff + +A minimal PaaS for FreeBSD + Nginx apps, in any language with a Makefile. +`git push` triggers a build, an optional app-defined release phase, a ZFS +snapshot, a cutover, and a health check - with automatic rollback to the +pre-deploy snapshot if the new release fails its health check. One binary, +no daemon, no separate metadata database - jail.conf, ZFS datasets, and a +couple of flat files per app are the only state. + +Skiff has no opinion on your language or runtime - the only thing it +requires of an app is a `Makefile` that produces an executable named `app`, +and that `app` accepts TCP traffic on an assigned port. See [What an app +needs to provide](#what-an-app-needs-to-provide). + +## Requirements + +- FreeBSD 15, ZFS root +- `nginx` and `git` - neither ships in base; install with + `pkg install nginx git-lite` +- `jail_enable="YES"` in `/etc/rc.conf` (`sysrc jail_enable=YES`) - `service + jail start`/`restart` refuse to run without it +- Whatever toolchain your apps' Makefiles invoke to build - `go`, `python`, + `rustc`, `node`, etc. - installed on the **host** (builds run outside the + jail; see [One-time host setup](#one-time-host-setup)). `make` itself is + already in FreeBSD base (bmake), so it's the one build tool Skiff never + asks you to install. + +## One-time host setup + +### ZFS layout + +``` +# zfs create zroot/skiff +# zfs create -o mountpoint=/skiff/base zroot/skiff/base +# bsdinstall jail /skiff/base +# zfs snapshot zroot/skiff/base@ready +# zfs create -o mountpoint=/skiff/apps zroot/skiff/apps +``` + +`ready` is the snapshot every app's dataset gets cloned from. Re-snapshot +under a new tag after patching the base (e.g. `security-2026-08`) and pass +`--base-snapshot` to `new-app` to pick it up. + +`zroot/skiff/apps` must exist as its own dataset, not a plain directory - +`new-app` clones straight into it (`zroot/skiff/apps/<name>`), and `zfs +clone` requires the parent dataset to already exist. + +### Repos directory + +``` +# mkdir -p /skiff/repos +``` + +### Nginx + +Add once to `/usr/local/etc/nginx/nginx.conf`, inside the `http` block: + +``` +include /usr/local/etc/nginx/skiff.d/*.conf; +``` + +``` +# mkdir -p /usr/local/etc/nginx/skiff.d +``` + +Skiff only ever writes files under `skiff.d/` and reloads - it never touches +`nginx.conf` itself. + +### Install skiff + +``` +$ GOOS=freebsd GOARCH=amd64 go build -o skiff ./cmd/skiff +# install -m 755 skiff /usr/local/sbin/skiff +``` + +## Deploying an app + +### First deploy + +``` +# skiff new-app --name myapp --ip 10.0.0.5 --port 8080 --server-name myapp.example.com +``` + +Clones the base dataset, writes `/etc/jail.conf.d/myapp.conf` and the jail's +own rc.d service, creates the bare repo at `/skiff/repos/myapp.git` with its +`post-receive` hook installed, and starts the (empty) jail. Prints the +remote URL to add locally. + +### Every deploy after + +``` +$ git remote add skiff root@host:/skiff/repos/myapp.git +$ git push skiff main +``` + +The push blocks until the deploy finishes - success or failure prints +straight to your terminal, no dashboard to check. + +### Rollback + +``` +# skiff rollback --app myapp --list +# skiff rollback --app myapp --to pre-a1b2c3d +``` + +Rolls the app's entire ZFS dataset back to that snapshot (code and any +release-phase side effects together) and restarts the jail. + +This uses `zfs rollback -r`, which **destroys every snapshot newer than the +target** - ZFS can't roll back past one otherwise. Rolling back to an old +snapshot permanently discards the ability to return to anything deployed +in between; there's no separate "undo the rollback." + +## What an app needs to provide + +A `Makefile` in the repo root, and nothing else Skiff cares about - no +required language, framework, or file layout beyond this contract: + +- Running bare `make` (Skiff runs `make -B`, so it never trusts a stale + build artifact left in the checkout; every deploy is a fresh worktree + anyway) must leave an executable file named `app` in the repo root. + What produces it is entirely up to you - `go build -o app .`, a + `cp target/release/myapp app` after `cargo build --release`, a shell + script with a `#!/usr/bin/env python3` shebang that `chmod +x`'d itself + into place, whatever. Skiff execs this file directly (via `daemon(8)`), + and the kernel doesn't care whether it's a native binary or a + script - only that it's marked executable and has a valid interpreter + line if it's not. +- That executable listens on the port given to `new-app` (bind the + wildcard address, not `127.0.0.1`), and answers the configured health + path with a non-3xx status once ready. +- Optionally, a `release` target in the same Makefile - run once inside + the jail *before* cutover (against the new release's own checkout, not + whatever `current` still points at), gating the deploy on its exit + code. Skiff doesn't interpret this target; it's entirely the app's own + convention (`sqlite3` migrations, cache warming, whatever). Detected by + asking `make` itself via `make -n release`, not by parsing the + Makefile - it can be defined however you like (directly, via an + include, a pattern rule). + +```makefile +.PHONY: all release +all: app + +app: main.go go.mod + go build -o app . + +release: + sqlite3 /app/data.db < migrations/0004_add_x.sql +``` + +See [USER_GUIDE.md](USER_GUIDE.md) for a full walkthrough with a real +example app. + +Runtime dependencies (a Python interpreter, the Node runtime, etc.) must +be present in the jail's base image if your `app` isn't a self-contained +static binary - that's a base-jail provisioning concern, not something +the Makefile handles. + +## Known gaps + +- No IP allocation - `--ip` is operator-chosen. Fine for a handful of apps + on one host; would want a small pool allocator beyond that. +- Old `releases/<sha>` directories are never pruned. The ZFS snapshot is the + real rollback mechanism; the release dirs are just there for the instant + symlink flip and are safe to clean up by hand. +- Single host only. Scaling to a second host is a `zfs send`/`receive` of an + app's dataset plus the same jail.conf/nginx steps - not yet automated here. |
