Back to all posts
5 min read

Blackship: A FreeBSD Jail Orchestrator That Understands State

Blackship: A FreeBSD Jail Orchestrator That Understands State

I’ve been running FreeBSD as my daily workstation for 2+ years, with jails handling everything from development environments to services. The tooling is fine. But “fine” doesn’t cut it when you have 15 jails with dependencies, need health checks, and want to know exactly what state each jail is in.

Blackship is a FreeBSD jail orchestrator with:

  • TOML configuration
  • Dependency management (topological ordering via petgraph)
  • State machine lifecycle control
  • ZFS integration (snapshots, clones, send/receive)
  • Health checks with circuit breaker protection
  • Docker-like Jailfile templates

Quick Start

# Install
cargo install blackship

# Create config
cat > blackship.toml << 'EOF'
[config]
data_dir = "/var/blackship"
zfs_enabled = true
zpool = "zroot"
dataset = "blackship"

[[jails]]
name = "web"
release = "15.0-RELEASE"
hostname = "web.local"

[jails.network]
vnet = true
bridge = "blackship0"
ip = "10.0.1.10"
gateway = "10.0.1.1"
EOF

# Bootstrap FreeBSD release
blackship bootstrap 15.0-RELEASE

# Create network
blackship network create default --subnet 10.0.1.0/24 --gateway 10.0.1.1 --bridge blackship0

# Launch
blackship up web

That’s a running jail with VNET networking.

What Makes It Different

State Machines, Not Flags

Every jail has explicit states:

stateDiagram-v2
    [*] --> Stopped
    Stopped --> Starting: start()
    Starting --> Running: started()
    Running --> Stopping: stop()
    Stopping --> Stopped: stopped()

    Starting --> Failed: fail()
    Running --> Failed: fail()
    Stopping --> Failed: fail()

    Failed --> Stopped: recover()

Invalid transitions are rejected. You can’t stop a jail that’s already stopped. You can’t start a jail that’s starting. The system knows what’s happening.

Dependency Ordering

[[jails]]
name = "app"
depends_on = ["cache", "database"]

[[jails]]
name = "cache"

[[jails]]
name = "database"

Run blackship up app:

  1. Start database
  2. Start cache
  3. Start app

Run blackship down app:

  1. Stop app
  2. Stop cache
  3. Stop database

Dry run first: blackship up app --dry-run

The Warden (Supervisor)

Auto-restart with exponential backoff and circuit breaker:

blackship supervise

When a jail crashes:

  1. Wait 1 second, restart
  2. If it crashes again, wait 2 seconds
  3. Then 4s, 8s, 16s… up to 60s max
  4. After 5 consecutive failures, circuit opens (no restarts for 5 minutes)
  5. After 5 minutes, try again in half-open state

No restart loops. No hammering the system.

ZFS Everything

# Snapshots
blackship snapshot create web pre-upgrade
blackship snapshot list web
blackship snapshot rollback web pre-upgrade --force
blackship snapshot delete web old-snap

# Clones
blackship clone web@pre-upgrade web-test

# Export/Import
blackship export web -o backup.tar.zst
blackship export web -o backup.zfs --zfs-send  # Fast
blackship import backup.tar.zst --name web-restored

Jailfile Templates

Build reproducible jails:

FROM 15.0-RELEASE

METADATA name=nginx version=1.0

ARG NGINX_VERSION=1.26
ENV NGINX_VERSION=${NGINX_VERSION}

RUN pkg install -y nginx-${NGINX_VERSION}
RUN sysrc nginx_enable=YES

COPY nginx.conf /usr/local/etc/nginx/nginx.conf
COPY html/ /usr/local/www/html/

WORKDIR /usr/local/www
EXPOSE 80/tcp
CMD /usr/local/sbin/nginx -g 'daemon off;'

Build it:

blackship build -f Jailfile -n nginx-jail
blackship build -f Jailfile -n nginx-jail --build-arg NGINX_VERSION=1.26

Health Checks

Command-based. Exit 0 = healthy.

[jails.healthcheck]
enabled = true

[[jails.healthcheck.checks]]
name = "http"
command = "curl -sf http://localhost:80/health"
target = "jail"
interval = 30
timeout = 10
retries = 3

[[jails.healthcheck.checks]]
name = "process"
command = "pgrep nginx"
target = "jail"

Monitor:

blackship health web
blackship health --watch --interval 5
blackship health --json  # For scripting

Lifecycle Hooks

Run scripts at specific phases:

[[jails.hooks]]
phase = "post_start"
target = "jail"
command = "/etc/rc.d/nginx start"
on_failure = "abort"

[[jails.hooks]]
phase = "pre_stop"
target = "jail"
command = "/etc/rc.d/nginx stop"
on_failure = "continue"

Phases: pre_create, post_create, pre_start, post_start, pre_stop, post_stop

Port Forwarding

Uses PF anchors (no manual /etc/pf.conf editing):

blackship expose web -p 80
blackship expose web -p 443 -I 192.168.1.100
blackship expose web -p 8080 --internal 80 --proto tcp
blackship ports

Add to /etc/pf.conf:

rdr-anchor "blackship"
anchor "blackship"

Full Example: Web Stack

[config]
data_dir = "/var/blackship"
zfs_enabled = true
zpool = "zroot"
dataset = "blackship"

[[jails]]
name = "postgres"
release = "15.0-RELEASE"
hostname = "db.local"
[jails.network]
vnet = true
bridge = "blackship0"
ip = "10.0.1.10"
gateway = "10.0.1.1"

[[jails]]
name = "redis"
release = "15.0-RELEASE"
hostname = "cache.local"
[jails.network]
vnet = true
bridge = "blackship0"
ip = "10.0.1.11"
gateway = "10.0.1.1"

[[jails]]
name = "webapp"
release = "15.0-RELEASE"
hostname = "app.local"
depends_on = ["postgres", "redis"]
[jails.network]
vnet = true
bridge = "blackship0"
ip = "10.0.1.20"
gateway = "10.0.1.1"
[jails.network.dns]
nameservers = ["8.8.8.8"]
mode = "custom"

[jails.healthcheck]
enabled = true
[[jails.healthcheck.checks]]
name = "http"
command = "curl -sf http://localhost:3000/health"
target = "jail"
interval = 30
blackship up --all
# Starts: postgres → redis → webapp

blackship down --all
# Stops: webapp → redis → postgres

Commands Reference

Lifecycle: up, down, restart, ps, check, init, cleanup

Console: console <jail>, exec <jail> -- <cmd>

Bootstrap: bootstrap <release>, releases list|delete|verify

Networking: network create|destroy|list, expose, ports

Snapshots: snapshot create|list|rollback|delete, clone

Export/Import: export, import

Build: build, template list|inspect|validate

Monitoring: health, supervise, logs

Shell Completion: completion bash|zsh|fish

Requirements

  • FreeBSD 14.0+
  • ZFS (optional, for snapshots/clones)
  • PF (optional, for port forwarding)

Install

# From crates.io
cargo install blackship

# Or download binary
fetch https://github.com/seuros/blackship/releases/latest/download/blackship-freebsd-amd64.tar.gz
tar xzf blackship-freebsd-amd64.tar.gz
mv blackship /usr/local/bin/

# Shell completion
blackship completion zsh > /usr/local/share/zsh/site-functions/_blackship

Why I Built This

Existing jail managers are either too simple (no dependencies, no state tracking) or too complex (full VM management when I just need jails).

I wanted:

  • Clean state machine lifecycle with explicit transitions
  • Automatic dependency ordering
  • Circuit breaker protection against restart loops
  • ZFS-first design without bolted-on abstractions
  • Docker-like build templates without the Docker overhead

Blackship is that.

Why “Blackship”?

The name comes from Warhammer 40K. In the Imperium, Black Ships are massive vessels operated by the Adeptus Astra Telepathica. Their purpose: transport dangerous psykers across the galaxy, containing them in specialized cells until they reach Terra.

The parallel to FreeBSD jails was too good to ignore:

  • Containment: Jails isolate processes like Black Ships isolate psykers
  • Transport: Export/import moves jails between systems like Black Ships move cargo between worlds
  • Control: State machines ensure nothing escapes containment unexpectedly

The internal naming follows the naval theme:

  • Warden: The supervisor daemon (guards the cargo)
  • Bridge: Central orchestration component (the ship’s command center)
  • Bulkhead: Isolation boundaries between jails
  • Sickbay: Health check system

No, it has nothing to do with race. It’s a 40K reference. The Emperor protects.

GitHub | MIT License

The bridge is yours. Launch your fleet.

🔗 Interstellar Communications

No transmissions detected yet. Be the first to establish contact!

• Link to this post from your site• Share your thoughts via webmention• Join the IndieWeb conversation

Related Posts