HostKit declarations are ordinary .exs files. The DSL evaluates to plain HostKit structs; evaluation does not apply changes.

Install

Add HostKit to a Mix project while it is unreleased or path-based in this workspace:

def deps do
  [
    {:host_kit, path: "../host_kit"}
  ]
end

Then fetch deps:

mix deps.get

Create a host config

# infra/config.exs
use HostKit.DSL

project :demo do
  host :local_vm, at: "192.0.2.10" do
    ssh do
      user "root"
      identity_file Path.expand("~/.ssh/id_ed25519")
      accept_hosts true
    end
  end

  bootstrap do
    package :ca_certificates
    package :curl

    mise do
      tool :erlang, "29.0.2"
      tool :elixir, "1.20.1"
    end
  end
end

Plan

mix host_kit.plan --host local_vm infra/config.exs

For deterministic package resolution, write a lock file:

mix host_kit.plan --host local_vm \
  --write-package-lock host_kit.package.lock \
  infra/config.exs

Review a plan artifact

mix host_kit.plan --host local_vm \
  --package-lock host_kit.package.lock \
  --out host_kit.plan.json \
  infra/config.exs

host_kit.plan.json is JSON, not an opaque binary. Review it before apply.

Apply

mix host_kit.apply --host local_vm \
  --plan host_kit.plan.json \
  --confirm \
  infra/config.exs

Use --dry-run instead of --confirm to exercise the apply path without changing the target.

Secrets

Control-plane secrets are represented as references:

ssh do
  user "root"
  password secret_env("HOSTKIT_SSH_PASSWORD")
  accept_hosts true
end

HostKit resolves the environment variable only when opening the SSH connection. Plan artifacts contain HOSTKIT_SSH_PASSWORD, not the password value.

Target application env files use contextual env declarations:

service :app do
  env :runtime do
    set :mix_env, :prod
    secret :database_url, env: "DATABASE_URL"
  end

  daemon do
    env :runtime
    exec ["/opt/app/bin/server"]
  end
end

Next steps