HostKit
Elixir-native host management: declare a Linux host, bootstrap packages and runtimes, isolate services with systemd, wire provider integrations, review a plan artifact, then apply it locally or over SSH.
HostKit is for operating real machines without assuming the target already has Elixir, Mix, Docker, or your application runtime installed.
Why HostKit
Infrastructure code should be boring Elixir, not an opaque pile of shell scripts.
HostKit gives you:
- Declarative host bootstrap — OS packages, accounts, directories, files, env files, systemd units, firewall rules, and
miseruntimes. - Docker-less service isolation — systemd sandboxing, resource limits, network policy, read/write path allowlists, loopback listeners, and managed env files.
- Plan before apply — read current state, produce a diff, write an inspectable JSON artifact, then apply exactly what was reviewed.
- Distribution-aware packages — semantic package names resolve through Repology and can be locked for deterministic applies.
- No hidden Mix requirement on target hosts — bootstrap can install prerequisites and BEAM tools through
mise. - Host config in
.exs— syntax highlighting, macros, composition, and project-local DSLs. - Provider boundary — integrations such as Caddy live as providers while core owns systemd/unitctl primitives.
- Linux-native integration testing — Incus containers/VMs replace macOS-only Lima flows on Linux.
One file: host, runtime, isolated service, reverse proxy
The complete example lives in examples/full_host.exs and is loaded by the test suite so it does not drift.
use HostKit.DSL, providers: [HostKit.Providers.Caddy]
project :prod do
host :app do
hostname "app.example.com"
user "root"
sudo true
ssh identity_file: Path.expand("~/.ssh/id_ed25519"), silently_accept_hosts: true
end
service :bootstrap do
package :ca_certificates
mise path: "/usr/local/bin/mise", system_data_dir: "/usr/local/share/mise" do
tool :erlang, "29.0.2"
tool :elixir, "1.20.1"
end
end
service :api do
account "api", system: true, home: "/var/lib/api"
directory "/var/lib/api", owner: "api", group: "api", mode: 0o750
env_file "/etc/api/api.env", owner: "root", group: "api" do
secret :database_url, env: "DATABASE_URL"
end
daemon "api.service" do
service_user "api"
environment_file "/etc/api/api.env"
exec_start ["/opt/api/bin/server"]
sandbox :strict_app,
resources: [memory_max: "512M"],
sandbox: [read_write_paths: ["/var/lib/api"]]
listen :http, port: 4000, on: :loopback
wanted_by :multi_user
end
caddy_site :api, "api.example.com" do
reverse_proxy listener(:http)
end
end
end
This compiles to inspectable HostKit structs and renders ordinary Linux primitives: packages, files, env files, accounts, systemd units, Caddy site config, and systemd hardening directives such as NoNewPrivileges=, ProtectSystem=, RestrictAddressFamilies=, ReadWritePaths=, and memory limits.
Plan, review, apply:
mix host_kit.plan --host app \
--write-package-lock host_kit.package.lock \
--out host_kit.plan.json \
infra/config.exs
mix host_kit.apply --host app \
--plan host_kit.plan.json \
--confirm \
infra/config.exs
secret_env/1 stores an environment-variable reference. Plan artifacts include the variable name, not the resolved secret value.
Interactive notebook
Deploy real services from Livebook with Kino inputs for SSH target/auth, plan review, explicit apply, and HTTP verification:
Static Caddy site:
Phoenix app from Git, with pinned source revision and source-aware build stamps:
The notebooks are self-contained and their deployment DSL cells are also exercised by the integration test suite.
Documentation
- Getting started
- Conventions and paths
- Remote bootstrap and plan artifacts
- Systemd isolation
- Firewall and networking
- Workspaces and tenants
- Observability and monitors
- Timers and jobs
- Deploy a Caddy site Livebook
- Deploy a Phoenix app Livebook
- CLI reference
- Full DSL/reference notes
- Internal architecture
- Changelog
Development
mix deps.get
mix ci
Run the Incus-backed remote integration on Linux:
HOSTKIT_INCUS_SUDO=true HOSTKIT_SSH_PUBLIC_KEY=$HOME/.ssh/id_ed25519.pub \
scripts/incus_integration_vm.sh ensure
HOSTKIT_INTEGRATION_TOOL=incus HOSTKIT_INCUS_SUDO=true \
mix test test/integration/cli_remote_test.exs --include integration
Status
HostKit is early and intentionally evolving. Runtime APIs come first; Mix tasks wrap them. DSLs compile to plain structs so plans and artifacts remain inspectable.
License
MIT