<feed xmlns='http://www.w3.org/2005/Atom'>
<title>skiff.git, branch master</title>
<subtitle>Unnamed repository; edit this file 'description' to name the repository.
</subtitle>
<id>https://git.mikeanthonywild.com/skiff.git/atom?h=master</id>
<link rel='self' href='https://git.mikeanthonywild.com/skiff.git/atom?h=master'/>
<link rel='alternate' type='text/html' href='https://git.mikeanthonywild.com/skiff.git/'/>
<updated>2026-09-03T02:53:45Z</updated>
<entry>
<title>Rate-limit the app process restart instead of hot-looping</title>
<updated>2026-09-03T02:53:45Z</updated>
<author>
<name>Claude</name>
<email>noreply@anthropic.com</email>
</author>
<published>2026-09-03T02:53:45Z</published>
<link rel='alternate' type='text/html' href='https://git.mikeanthonywild.com/skiff.git/commit/?id=09b35fe0564cefa304f5460e2bf95420e9f3ed96'/>
<id>urn:sha1:09b35fe0564cefa304f5460e2bf95420e9f3ed96</id>
<content type='text'>
daemon -r restarts immediately with no delay - a genuinely broken deploy
(crashes on every start) would hot-loop as fast as the kernel can
fork/exec, burning CPU and flooding syslog. -R 5 restarts with a 5s gap
instead, matching both Heroku's dyno manager (backs off a repeatedly-
crashing dyno rather than hammering it) and dvd.club's own pre-skiff rc.d
script, which already used -R 5 before this template replaced it.

Verified on the VM: killed the app process directly, confirmed it stayed
down through the 1s and 4s marks and was back by 7s - not an immediate
restart.

Co-Authored-By: Claude Sonnet 5 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>Clarify kldload pf is only needed for the dry-run check, not service start</title>
<updated>2026-09-03T02:48:47Z</updated>
<author>
<name>Claude</name>
<email>noreply@anthropic.com</email>
</author>
<published>2026-09-03T02:48:47Z</published>
<link rel='alternate' type='text/html' href='https://git.mikeanthonywild.com/skiff.git/commit/?id=76c238741e845eec94973a4c280449c072fad93d'/>
<id>urn:sha1:76c238741e845eec94973a4c280449c072fad93d</id>
<content type='text'>
Verified on the VM: service pf start auto-loads the kernel module itself
(required_modules="pf" in /etc/rc.d/pf, which rc.subr honors for start
specifically) - service pf check and bare pfctl don't get the same
treatment, so kldload is only necessary if you want to dry-run the
ruleset before committing to it. The previous wording implied it was
needed either way.

Co-Authored-By: Claude Sonnet 5 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>Flesh out the single-public-IP pf setup with what actually worked</title>
<updated>2026-09-03T02:45:25Z</updated>
<author>
<name>Claude</name>
<email>noreply@anthropic.com</email>
</author>
<published>2026-09-03T02:45:25Z</published>
<link rel='alternate' type='text/html' href='https://git.mikeanthonywild.com/skiff.git/commit/?id=48d4c516c81dc3c1394ebea5ecd5aa588bc6960a'/>
<id>urn:sha1:48d4c516c81dc3c1394ebea5ecd5aa588bc6960a</id>
<content type='text'>
Replaces the sketch (bare ifconfig alias, isolated nat line) with what we
verified end-to-end on a real VM: the alias persisted via rc.conf, a full
pf.conf modeled on a plain host's firewall, and the pfctl/kldload steps
needed to load and check it safely.

Also documents a real bug hit along the way: `nat ... -&gt; ($ext_if)` is
ambiguous once the interface carries more than one address (the public
IP and the private jail range both live on it) - pf can silently NAT
through the wrong one. The public IP needs to be written explicitly.

Co-Authored-By: Claude Sonnet 5 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>Document jail networking on a single public IP</title>
<updated>2026-09-03T02:37:50Z</updated>
<author>
<name>Claude</name>
<email>noreply@anthropic.com</email>
</author>
<published>2026-09-03T02:37:50Z</published>
<link rel='alternate' type='text/html' href='https://git.mikeanthonywild.com/skiff.git/commit/?id=a7b8edcf866269a8bc389abd411e6f7e0cfe0252'/>
<id>urn:sha1:a7b8edcf866269a8bc389abd411e6f7e0cfe0252</id>
<content type='text'>
The --ip same-subnet rule from earlier is necessary but not sufficient
once a host only has one public address: jails shouldn't get public IPs
at all (only nginx needs one), but a private range aliased onto the same
interface needs its own outbound NAT rule for jail builds to reach the
internet - nothing upstream translates it the way a NAT'd virtual network
(like the dev VM) does for free. Skipping it fails the same silent way as
the out-of-subnet case: packets leave the jail, nothing answers.

Co-Authored-By: Claude Sonnet 5 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>Rename the default base snapshot from "ready" to "default"</title>
<updated>2026-09-03T02:11:01Z</updated>
<author>
<name>Claude</name>
<email>noreply@anthropic.com</email>
</author>
<published>2026-09-03T02:11:01Z</published>
<link rel='alternate' type='text/html' href='https://git.mikeanthonywild.com/skiff.git/commit/?id=9f3f9ee638e0a82960446f74345e9ee99bfec474'/>
<id>urn:sha1:9f3f9ee638e0a82960446f74345e9ee99bfec474</id>
<content type='text'>
"ready" read as a status word rather than a label for what's actually a
mutable pointer to "the current recommended base image," re-tagged as it
evolves - the exact operation we did twice this session. "default" names
that role directly: it's the value --base-snapshot falls back to, nothing
more claimed.

Co-Authored-By: Claude Sonnet 5 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>Use the go meta-port instead of a hand-rolled go/gofmt symlink</title>
<updated>2026-09-03T02:07:32Z</updated>
<author>
<name>Claude</name>
<email>noreply@anthropic.com</email>
</author>
<published>2026-09-03T02:07:32Z</published>
<link rel='alternate' type='text/html' href='https://git.mikeanthonywild.com/skiff.git/commit/?id=f10cd386fd85386303253287d3e0c5e2a7d52124'/>
<id>urn:sha1:f10cd386fd85386303253287d3e0c5e2a7d52124</id>
<content type='text'>
FreeBSD's lang/go is a real meta-port (installs as package name "go")
that depends on whichever goNNN is current and installs the go/gofmt
symlinks itself, tracked and upgradeable - `pkg install go` replaces both
the go125 install and the manual `ln -sf` steps in one command. Verified
against a real base image and rebuild: same resulting symlinks, now
pkg-owned instead of orphaned files pkg doesn't know about.

Docs updated to check for an unversioned meta-port first, falling back to
a manual symlink only when a toolchain doesn't have one.

Co-Authored-By: Claude Sonnet 5 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>Drop unused --old flag from skiff deploy</title>
<updated>2026-09-03T02:03:17Z</updated>
<author>
<name>Claude</name>
<email>noreply@anthropic.com</email>
</author>
<published>2026-09-03T02:03:17Z</published>
<link rel='alternate' type='text/html' href='https://git.mikeanthonywild.com/skiff.git/commit/?id=bd25a73ca5e801af4a9087597c15b1958d1c6588'/>
<id>urn:sha1:bd25a73ca5e801af4a9087597c15b1958d1c6588</id>
<content type='text'>
Its only caller was the post-receive hook skiff itself generates, so
"hook parity" meant matching a convention we invented and fully control -
there's no external contract keeping it around. deploy.Run never read it.

Co-Authored-By: Claude Sonnet 5 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>Add domain and TLS management via skiff domains add/remove/list</title>
<updated>2026-09-03T02:00:08Z</updated>
<author>
<name>Claude</name>
<email>noreply@anthropic.com</email>
</author>
<published>2026-09-03T02:00:08Z</published>
<link rel='alternate' type='text/html' href='https://git.mikeanthonywild.com/skiff.git/commit/?id=6040eff2d2b4618f19d4615d246ea6b0704acdf8'/>
<id>urn:sha1:6040eff2d2b4618f19d4615d246ea6b0704acdf8</id>
<content type='text'>
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 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>Split per-app dataset into OS/data layers, add skiff rebuild</title>
<updated>2026-08-26T04:04:10Z</updated>
<author>
<name>Mike Wild</name>
<email>mike@mikeanthonywild.com</email>
</author>
<published>2026-08-26T04:04:10Z</published>
<link rel='alternate' type='text/html' href='https://git.mikeanthonywild.com/skiff.git/commit/?id=025a365a23f5ed2b3df4df71cbff9689575968d9'/>
<id>urn:sha1:025a365a23f5ed2b3df4df71cbff9689575968d9</id>
<content type='text'>
Each app now gets two ZFS datasets - a disposable OS layer
(zroot/skiff/appbase/&lt;app&gt;) cloned from base, and a persistent data
layer (zroot/skiff/appdata/&lt;app&gt;) holding releases/current/env/
meta.json/data.db. `skiff rebuild --app|--all` discards and reclones
the OS layer to pick up an OS/toolchain/config-template upgrade,
guarded by the same snapshot-and-rollback safety net as deploy and
config changes; the data layer is never touched.

Also fixes stale docs (builds already run via jexec inside the jail,
not on the host; --iface already auto-aliases the jail's IP, so the
manual ifconfig step and its troubleshooting entry are gone) found
while updating the same sections for this feature.

Co-Authored-By: Claude Sonnet 5 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>new-app: configure jail mail relay and persist the jail's network alias</title>
<updated>2026-08-26T02:23:04Z</updated>
<author>
<name>Claude</name>
<email>noreply@anthropic.com</email>
</author>
<published>2026-08-26T02:23:04Z</published>
<link rel='alternate' type='text/html' href='https://git.mikeanthonywild.com/skiff.git/commit/?id=2a9ca0577ecdbee8b5a4bc8886c112dbc4260eab'/>
<id>urn:sha1:2a9ca0577ecdbee8b5a4bc8886c112dbc4260eab</id>
<content type='text'>
Two provisioning gaps in new-app:

- The jail's dma.conf inherited the base image's shared SMARTHOST/
  PORT/AUTHPATH, but never got a per-app MAILNAME - without one, dma
  can misroute mail for recipients on the jail's own apex domain as
  local delivery. EnableMail appends a MAILNAME of &lt;app&gt;.skiff.local,
  which can't collide with any real domain an app sends to or from.

- A classic jail's ip4.addr is only a bind restriction, not a route,
  until something aliases it onto a host interface. jail.conf.tmpl
  never set jail(8)'s own `interface` parameter, so nothing ever
  created that alias - confirmed on skiff-freebsd, where the address
  had twice been made to work via an ad hoc `ifconfig alias` that
  didn't survive a reboot. Setting `interface` lets jail(8) manage
  the alias itself, including on every boot.
</content>
</entry>
</feed>
