| Crates.io | blackship |
| lib.rs | blackship |
| version | 0.1.4 |
| created_at | 2025-12-24 22:19:01.073429+00 |
| updated_at | 2026-01-25 17:03:52.345948+00 |
| description | A FreeBSD jail manager with state machine control |
| homepage | |
| repository | https://github.com/seuros/blackship |
| max_upload_size | |
| id | 2003927 |
| size | 518,594 |
A FreeBSD jail orchestrator with TOML configuration, dependency management, state machine lifecycle control, and ZFS integration.
# Install from crates.io
cargo install blackship
# Or download precompiled binary from releases
fetch https://github.com/seuros/blackship/releases/latest/download/blackship-freebsd-amd64.tar.gz
tar xzf blackship-freebsd-amd64.tar.gz
sudo mv blackship /usr/local/bin/
# Install shell completion (optional)
blackship completion bash > /usr/local/etc/bash_completion.d/blackship
blackship completion zsh > /usr/local/share/zsh/site-functions/_blackship
blackship completion fish > ~/.config/fish/completions/blackship.fish
Blackship uses XDG-compliant user directories by default:
| Path | Default | Environment Override |
|---|---|---|
data_dir |
~/.local/share/blackship |
XDG_DATA_HOME |
releases_dir |
~/.local/share/blackship/releases |
- |
cache_dir |
~/.cache/blackship |
XDG_CACHE_HOME |
These defaults allow running blackship without root for most operations. Override in blackship.toml for system-wide paths:
[config]
data_dir = "/var/blackship"
releases_dir = "/var/blackship/releases"
cache_dir = "/var/blackship/cache"
Note: While paths are user-writable by default,
bootstrapextraction requires root privileges because FreeBSD base archives contain root-owned files with special permissions (setuid binaries, etc.). Usesudo blackship bootstrap <release>for extraction.
# Create a Jailfile in current directory
blackship init
# Create with specific FreeBSD release
blackship init --release 15.0-RELEASE
# TOML format instead of Dockerfile-like
blackship init --toml
Create blackship.toml:
[config]
# Paths default to XDG directories (~/.local/share/blackship, ~/.cache/blackship)
# Uncomment to use system-wide paths:
# 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"
Or use blackship armada init to generate a template.
# Download and extract FreeBSD base (requires root for extraction)
sudo blackship bootstrap 15.0-RELEASE
# Dry run to see what would be downloaded
blackship bootstrap 15.0-RELEASE --dry-run
# List available releases
blackship releases
# Create bridge with gateway
blackship network create default --subnet 10.0.1.0/24 --gateway 10.0.1.1 --bridge blackship0
# Start a specific jail
blackship up web
# Start all jails
blackship up --all
# Dry run (show what would happen)
blackship up --all --dry-run
# Open console
blackship console web
# Execute command
blackship exec web -- pkg install -y nginx
# Check status
blackship ps
blackship ps --json
[config]
# All paths are optional - defaults to XDG directories
data_dir = "~/.local/share/blackship" # Base data directory (default)
releases_dir = "~/.local/share/blackship/releases" # FreeBSD releases (default)
cache_dir = "~/.cache/blackship" # Download cache (default)
mirror_url = "https://download.freebsd.org" # FreeBSD mirror
zfs_enabled = true # Enable ZFS features
zpool = "zroot" # ZFS pool name
dataset = "blackship" # Base dataset name
[[jails]]
name = "myapp" # Jail name (required)
release = "15.0-RELEASE" # Base release
path = "/jails/myapp" # Custom path (optional)
hostname = "myapp.local" # Hostname
depends_on = ["database"] # Dependencies
[jails.network]
vnet = true # Enable VNET
bridge = "blackship0" # Bridge interface
ip = "10.0.1.10" # Static IP
gateway = "10.0.1.1" # Default gateway
mac_address = "02:00:00:00:00:01" # Static MAC (optional)
[jails.network.dns]
nameservers = ["8.8.8.8", "8.8.4.4"] # DNS servers
mode = "custom" # custom or inherit
[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"
[[jails.hooks]]
phase = "post_start"
target = "jail"
command = "/etc/rc.d/nginx start"
on_failure = "abort" # abort or continue
[[jails.hooks]]
phase = "pre_stop"
target = "jail"
command = "/etc/rc.d/nginx stop"
on_failure = "continue"
| Command | Description |
|---|---|
blackship up [jail] [--all] [--dry-run] |
Start jail(s) with dependencies |
blackship down [jail] [--all] [--dry-run] |
Stop jail(s) in reverse order |
blackship restart [jail] [--all] [--dry-run] |
Restart jail(s) |
blackship ps [--json] |
List jail status |
blackship check |
Validate configuration |
blackship setup |
Initialize PF firewall anchor |
blackship cleanup <jail> [--force] |
Clean up failed jail resources |
blackship init [-f file] [--release] [--toml] |
Create a new Jailfile |
| Command | Description |
|---|---|
blackship console <jail> [-u user] |
Open interactive shell |
blackship exec <jail> [-u user] [-w dir] [-e K=V] -- <cmd> |
Execute command in jail |
blackship run --name <n> --release <r> [-d] -- <cmd> |
Run ephemeral jail (auto-cleanup unless -d) |
| Command | Description |
|---|---|
blackship cp <src> <dest> |
Copy files (use jail:path for jail paths) |
blackship rm <jails...> [-f] [--volumes] |
Remove/destroy jails |
| Command | Description |
|---|---|
blackship bootstrap <release> [-f] [-a archives] |
Download FreeBSD release |
blackship releases [list|delete|verify] [--json] |
Manage releases |
| Command | Description |
|---|---|
blackship network create <name> -s <subnet> [-g gw] [-b bridge] |
Create network |
blackship network destroy <name> [--force] |
Destroy network |
blackship network list |
List networks |
blackship expose <jail> -p <port> [-I bind-ip] [--proto tcp|udp] |
Expose port |
blackship ports [jail] |
List exposed ports |
| Command | Description |
|---|---|
blackship snapshot create <jail> [name] |
Create snapshot |
blackship snapshot list <jail> [--json] |
List snapshots |
blackship snapshot rollback <jail> <snap> [--force] |
Rollback to snapshot |
blackship snapshot delete <jail> <snap> |
Delete snapshot |
blackship clone <jail>@<snap> <newname> |
Clone from snapshot |
| Command | Description |
|---|---|
blackship export <jail> [-o file] [--zfs-send] |
Export to archive |
blackship import <file> [-n name] [--force] |
Import from archive |
| Command | Description |
|---|---|
blackship build [-f Jailfile] [-n name] [--build-arg K=V] [--dry-run] |
Build from Jailfile |
blackship template list |
List templates |
blackship template inspect <file> |
Show Jailfile details |
blackship template validate <file> |
Validate Jailfile |
| Command | Description |
|---|---|
blackship health [jail] [-w] [-i interval] [--json] |
Health check status |
blackship supervise |
Start Warden supervisor for auto-restart |
blackship logs <jail> [-f] [-n lines] |
Tail jail logs |
| Command | Description |
|---|---|
blackship armada init [-f file] |
Create a new blackship.toml |
blackship armada up [-d] [jails...] |
Start all jails |
blackship armada down [jails...] |
Stop all jails |
blackship armada build [jails...] |
Build jails from Jailfiles |
blackship armada ps [--json] |
Show status of all jails |
blackship armada config [--show] |
Validate and show configuration |
Armada supports merging multiple config files (later files override earlier):
# Merge base config with production overrides
blackship armada -f base.toml -f prod.toml up
# Merge multiple files
blackship armada -f base.toml -f secrets.toml -f local.toml up
Jails in blackship.toml can reference Jailfiles for building:
[[jails]]
name = "web"
build = "./web" # Directory containing Jailfile
depends_on = ["db"]
[[jails]]
name = "api"
jailfile = "./custom/Api.jailfile" # Explicit Jailfile path
| Command | Description |
|---|---|
blackship completion bash|zsh|fish |
Generate shell completion |
Jailfiles define reproducible jail builds, similar to Dockerfiles:
# Jailfile
FROM 15.0-RELEASE
# Metadata
METADATA name=nginx-jail version=1.0
# Build arguments
ARG NGINX_VERSION=1.24
# Environment variables
ENV NGINX_VERSION=${NGINX_VERSION}
# Run commands (executed via jexec)
RUN pkg install -y nginx-${NGINX_VERSION}
RUN sysrc nginx_enable=YES
# Copy files from build context
COPY nginx.conf /usr/local/etc/nginx/nginx.conf
COPY html/ /usr/local/www/html/
# Set working directory
WORKDIR /usr/local/www
# Expose ports (documentation)
EXPOSE 80/tcp
EXPOSE 443/tcp
# Default command
CMD /usr/local/sbin/nginx -g 'daemon off;'
# Jailfile.toml
[metadata]
name = "nginx-jail"
version = "1.0"
from = "15.0-RELEASE"
[[args]]
name = "NGINX_VERSION"
default = "1.24"
[[instructions]]
type = "run"
command = "pkg install -y nginx"
[[instructions]]
type = "copy"
src = "nginx.conf"
dest = "/usr/local/etc/nginx/nginx.conf"
[[expose]]
port = 80
protocol = "tcp"
cmd = "/usr/local/sbin/nginx -g 'daemon off;'"
# Basic build
blackship build -f Jailfile -n myjail
# With build arguments
blackship build -f Jailfile -n myjail --build-arg NGINX_VERSION=1.26
# Dry run
blackship build -f Jailfile --dry-run
When zfs_enabled = true, Blackship:
zpool/blackship/jails/<name># Create snapshot before changes
blackship snapshot create web pre-update
# Make changes
blackship exec web -- pkg upgrade -y
# If something breaks, rollback
blackship down web
blackship snapshot rollback web pre-update --force
blackship up web
# Clone for testing
blackship clone web@pre-update web-test
# Fast export using ZFS send
blackship export web -o web-backup.zfs --zfs-send
# Standard tar.zst export
blackship export web -o web-backup.tar.zst
# Import (auto-detects format)
blackship import web-backup.tar.zst --name web-restored
Blackship uses VNET jails with epair interfaces connected to a bridge:
Host Bridge (blackship0)
├── epair0a ←→ epair0b (jail: web, 10.0.1.10)
├── epair1a ←→ epair1b (jail: db, 10.0.1.11)
└── gateway: 10.0.1.1
Uses PF anchors to avoid modifying /etc/pf.conf:
# Expose nginx on port 80
blackship expose web -p 80
# Expose on specific host IP
blackship expose web -p 443 -I 192.168.1.100
# Different internal port
blackship expose web -p 8080 --internal 80
Add to /etc/pf.conf:
rdr-anchor "blackship"
anchor "blackship"
All health checks are command-based (exit 0 = healthy).
[[jails.healthcheck.checks]]
name = "api"
command = "curl -sf http://localhost:8080/health"
target = "jail"
interval = 30
timeout = 10
[[jails.healthcheck.checks]]
name = "postgres"
command = "nc -z localhost 5432"
target = "jail"
[[jails.healthcheck.checks]]
name = "nginx-running"
command = "service nginx status"
target = "jail"
# One-time check
blackship health web
# Watch mode (updates every 5 seconds)
blackship health --watch --interval 5
# JSON output for scripting
blackship health --json
Jails start in dependency order and stop in reverse:
[[jails]]
name = "app"
depends_on = ["cache", "database"]
[[jails]]
name = "cache"
[[jails]]
name = "database"
# Starts: database → cache → app
blackship up app
# Stops: app → cache → database
blackship down app
[config]
# data_dir defaults to ~/.local/share/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
# Create snapshot
blackship snapshot create webapp v1.0
# Export for migration
blackship export webapp -o webapp-v1.0.tar.zst
# On new host
blackship import webapp-v1.0.tar.zst --name webapp
# Edit blackship.toml to add jail config
blackship up webapp
This is an example of how Blackship could be used as a backend for CI/CD systems. This is not a built-in integration - it demonstrates how the run, exec, cp, and rm commands can be combined for CI/CD workflows.
Blackship could be used as a backend for Gitea Actions via act_runner.
blackship bootstrap 15.0-RELEASE
runner:
labels:
- "freebsd-15:jail://15.0-RELEASE"
runs-on: freebsd-15 would execute in ephemeral jails.When act_runner receives a job with a jail:// label, the runner could:
blackship run --name gitea-runner-<id> --release <release> --detachblackship cpblackship exec <jail> --workdir /workspace -- <command>blackship rm <jail> --forceNote: This requires custom act_runner configuration or a wrapper script - it is not built into act_runner by default.
# Check configuration
blackship check
# Try dry run
blackship up myjail --dry-run
# Clean up failed resources
blackship cleanup myjail --force
# List bridges
blackship network list
# Check jail IP
blackship exec myjail -- ifconfig
# Verify routing
blackship exec myjail -- netstat -rn
# Verify dataset exists
zfs list | grep blackship
# Check snapshots
blackship snapshot list myjail
# Manual cleanup
zfs destroy -r zroot/blackship/jails/myjail
BSD-3-Clause - See LICENSE file for details.
Abdelkader Boudih oss@seuros.com