summaryrefslogtreecommitdiff
path: root/README.md
diff options
context:
space:
mode:
authorClaude <noreply@anthropic.com>2026-08-05 23:14:29 -0500
committerClaude <noreply@anthropic.com>2026-08-05 23:14:29 -0500
commit9f7c9f2f31acd7bc544dd988af1c8c2fe63ec504 (patch)
tree6d7ab2a965c3e138a56c625197658ef2b324b045 /README.md
Initial commit of skiff scaffolding
Diffstat (limited to 'README.md')
-rw-r--r--README.md168
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.