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 mise runtimes.
  • 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:

Run Caddy notebook in Livebook

Phoenix app from Git, with pinned source revision and source-aware build stamps:

Run Phoenix notebook in Livebook

The notebooks are self-contained and their deployment DSL cells are also exercised by the integration test suite.

Documentation

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