summaryrefslogtreecommitdiff
path: root/README.md
diff options
context:
space:
mode:
authorClaude <noreply@anthropic.com>2026-09-02 21:00:08 -0500
committerClaude <noreply@anthropic.com>2026-09-02 21:00:08 -0500
commit6040eff2d2b4618f19d4615d246ea6b0704acdf8 (patch)
tree1937c2607a5e980e2d9db4c0df7515a699f0c93a /README.md
parent025a365a23f5ed2b3df4df71cbff9689575968d9 (diff)
Add domain and TLS management via skiff domains add/remove/list
Replaces the single server_name string in meta.json with a domain list (one primary, others redirect to it) and TLSDomains tracking what the app's certificate actually covers. Certs are issued/expanded via certbot certonly (webroot, never the --nginx plugin, since Skiff owns and regenerates skiff.d/*.conf itself) as one SAN cert per app, matching how a real multi-domain vhost is normally issued. Domain/TLS changes never touch the jail - no restart, no health check - since they only affect nginx and the certificate. `add` is safe to retry if DNS isn't ready yet; `remove` never calls certbot at all, so it can't fail on an ACME/network problem. Also updates README/USER_GUIDE for the new command surface, one-time TLS host setup (certbot register, renewal cron, reload hook), the --ip same-subnet requirement for jails to reach the network at build time, and the actual base-image provisioning sequence (pkg needs an explicit bootstrap after bsdinstall; versioned port names like go125 need a default symlink) - all found and fixed live while getting this working end-to-end on a real FreeBSD VM. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Diffstat (limited to 'README.md')
-rw-r--r--README.md111
1 files changed, 109 insertions, 2 deletions
diff --git a/README.md b/README.md
index 4373008..006d67a 100644
--- a/README.md
+++ b/README.md
@@ -17,13 +17,23 @@ needs to provide](#what-an-app-needs-to-provide).
- FreeBSD 15, ZFS root
- `nginx` and `git` - neither ships in base; install with
`pkg install nginx git-lite`
+- `certbot` - only needed if you'll route real domains via `skiff domains`;
+ install with `pkg install py312-certbot` (package name tracks the default
+ Python version, so this may drift)
- `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. - baked into the base jail image (see [One-time host
setup](#one-time-host-setup)); builds run inside the jail via `jexec`, not
on the host itself. `make` itself is already in FreeBSD base (bmake), so
- it's the one build tool Skiff never asks you to install.
+ it's the one build tool Skiff never asks you to install. Watch for
+ versioned port names (`go125`, `python312`, ...) - FreeBSD installs the
+ literal versioned binary, not a bare `go`/`python3`, so symlink a default
+ into the base image's `/usr/local/bin` if a Makefile invokes the
+ unversioned name.
+- The build step needs real outbound network access from inside the jail
+ (module/package downloads) - see the `--ip` note under [First
+ deploy](#first-deploy).
## One-time host setup
@@ -33,6 +43,42 @@ needs to provide](#what-an-app-needs-to-provide).
# zfs create zroot/skiff
# zfs create -o mountpoint=/skiff/base zroot/skiff/base
# bsdinstall jail /skiff/base
+```
+
+`bsdinstall jail` only lays down the bare base system - it ships `pkg(8)`'s
+bootstrap stub but never actually runs it, so a jail cloned from this alone
+has no working `pkg` of its own (files can still be `pkg -c`-installed into
+it from the host, but nothing *inside* the jail can manage them, and
+`jexec <app> pkg ...` fails with "the package management tool is not yet
+installed"). Bootstrap pkg into the image itself, before snapshotting:
+
+```
+# mount -t devfs devfs /skiff/base/dev
+# env ASSUME_ALWAYS_YES=yes pkg -c /skiff/base bootstrap -f
+```
+
+(`-f` because the stub already counts as "pkg is present" otherwise; devfs
+is needed for pkg's own network/TLS calls inside a bare chroot - `fetch(1)`
+wants `/dev/null` and `/dev/random`.) Now install whatever your apps'
+Makefiles actually need to build:
+
+```
+# pkg -c /skiff/base install -y go125
+```
+
+Watch for versioned port names - FreeBSD installs `go125` as literally
+`go125`, not `go`, so a Makefile invoking the bare command needs a default
+symlinked in:
+
+```
+# ln -sf go125 /skiff/base/usr/local/bin/go
+# ln -sf gofmt125 /skiff/base/usr/local/bin/gofmt
+```
+
+Unmount devfs and snapshot once the image can actually build and run apps:
+
+```
+# umount /skiff/base/dev
# zfs snapshot zroot/skiff/base@ready
# zfs create -o mountpoint=none zroot/skiff/appbase
# zfs create -o mountpoint=none zroot/skiff/appdata
@@ -92,6 +138,32 @@ include /usr/local/etc/nginx/skiff.d/*.conf;
Skiff only ever writes files under `skiff.d/` and reloads - it never touches
`nginx.conf` itself.
+### TLS (Let's Encrypt)
+
+Skiff issues certificates itself via `certbot certonly` (webroot validation,
+never certbot's own `--nginx` plugin - Skiff owns `skiff.d/*.conf` and would
+fight any other writer of it). Register the certbot account once:
+
+```
+# certbot register --agree-tos -m you@example.com
+```
+
+Renewal rides on FreeBSD's own `periodic(8)`, same as any other certbot
+install - Skiff runs no daemon or scheduler of its own for this:
+
+```
+# sysrc -f /etc/periodic.conf weekly_certbot_enable="YES"
+```
+
+`certonly` mode doesn't reload nginx after a renewal the way certbot's
+installer plugins do, so add that as a deploy hook once:
+
+```
+# mkdir -p /usr/local/etc/letsencrypt/renewal-hooks/deploy
+# printf '#!/bin/sh\nnginx -s reload\n' > /usr/local/etc/letsencrypt/renewal-hooks/deploy/reload-nginx
+# chmod +x /usr/local/etc/letsencrypt/renewal-hooks/deploy/reload-nginx
+```
+
### Install skiff
```
@@ -104,7 +176,7 @@ $ GOOS=freebsd GOARCH=amd64 go build -o skiff ./cmd/skiff
### First deploy
```
-# skiff new-app --name myapp --ip 10.0.0.5 --port 8080 --server-name myapp.example.com
+# skiff new-app --name myapp --ip 192.168.1.50 --port 8080
```
Clones the base dataset, writes `/etc/jail.conf.d/myapp.conf` and the jail's
@@ -112,6 +184,38 @@ 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.
+`--ip` must actually route on the host's `--iface` interface - in practice,
+an address in the same subnet as the host's own primary address on that
+interface, not an arbitrary block like `10.0.0.0/8`. Classic (non-VNET)
+jails share the host's routing table, so an out-of-subnet address can still
+send packets - they just may never get a response, silently, if whatever
+sits upstream (a NAT gateway, a virtualized bridge) only answers its own
+subnet. This didn't matter before builds moved inside the jail; now every
+deploy's build step needs real outbound network access from that exact
+address (module downloads, `pkg install`, etc).
+
+### Domains & TLS
+
+`new-app` only provisions the jail - nothing is routed until you add a
+domain:
+
+```
+# skiff domains add --app myapp --primary myapp.example.com
+# skiff domains add --app myapp www.myapp.example.com
+# skiff domains list --app myapp
+# skiff domains remove --app myapp www.myapp.example.com
+```
+
+`add` routes the hostname over plain HTTP immediately, then requests or
+expands a single certificate covering every domain currently registered for
+the app (named after whichever is primary). If DNS isn't pointed at the
+app's `--ip` yet, issuance fails and the command exits non-zero - the app
+stays reachable over HTTP, and `add` is safe to re-run once DNS is fixed,
+same as retrying a failed deploy. The first domain added becomes primary
+automatically; every other domain 301-redirects to it, over HTTP and HTTPS
+alike. `remove` only updates routing - it never calls out to certbot, so a
+DNS or network problem can't make it fail.
+
### Every deploy after
```
@@ -209,3 +313,6 @@ the Makefile handles.
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.
+- `skiff domains remove` doesn't shrink the shared certificate's SAN - a
+ removed hostname may still be listed on it until the next `add` re-syncs
+ the domain set. Harmless, since nothing routes to it anymore.