ReleaseKit
ReleaseKit builds deployment-neutral artifacts from Mix OTP releases.
It is for Elixir applications that want a repeatable build product without tying that build product to a deploy tool. ReleaseKit produces ordinary files:
- an OTP release tarball;
- an ETF manifest describing the tarball, runtime command, runtime environment hints, and optional HTTP health check.
Deployment tools such as HostKit can consume the manifest, but ReleaseKit does not know about systemd, users, Caddy, hosts, or filesystem layouts.
Why use ReleaseKit?
A deploy pipeline usually needs two separate responsibilities:
- Build an application artifact from source.
- Install and supervise that artifact on a host.
ReleaseKit handles only the first responsibility. It gives downstream deploy systems a small, stable manifest instead of asking every application to invent a custom release tarball format or wrapper Mix task.
Installation
def deps do
[
{:release_kit, "~> 0.2.0", only: [:dev, :prod], runtime: false}
]
end
Quick start
Build a production artifact for the default Mix release:
MIX_ENV=prod mix release_kit.artifact --out-dir _build/prod/artifacts
For a web service, record health-check metadata:
MIX_ENV=prod mix release_kit.artifact \
--out-dir _build/prod/artifacts \
--port 4000 \
--health-path /
The output names are stable for deployment tooling and include a checksum sidecar:
_build/prod/artifacts/my_app-20260620-abcdef0.tar.gz
_build/prod/artifacts/my_app-20260620-abcdef0.tar.gz.sha256
_build/prod/artifacts/my_app.etf
Configuration
You can put artifact defaults in application config and keep the command short:
# config/config.exs
config :release_kit, :artifact,
port: 4000,
health_path: "/",
env_clear: %{
"MY_APP_WEB" => "true",
"MY_APP_PORT" => "4000",
"RELEASE_DISTRIBUTION" => "none"
}
Then build with:
MIX_ENV=prod mix release_kit.artifact --out-dir _build/prod/artifacts
CLI flags override config values when provided.
Phoenix and Volt assets
Phoenix applications that use Volt can configure a first-class asset preset.
ReleaseKit installs locked npm packages, runs mix volt.build, records asset
metadata in the manifest, and then packages the OTP release:
config :release_kit, :artifact,
port: 4000,
health_path: "/",
assets: [
volt: [profile: :my_app_web, tailwind: true, root: "assets", production: true, frozen: true]
]
For a flat Volt config, omit the profile:
config :release_kit, :artifact,
assets: [volt: [tailwind: true]]
For umbrellas or multiple web apps, build multiple profiles:
config :release_kit, :artifact,
assets: [
volt: [
profiles: [
[profile: :public_web, root: "apps/public_web/assets", tailwind: true],
[profile: :admin_web, root: "apps/admin_web/assets", tailwind: true]
]
]
]
Lifecycle steps
Some applications need generated files inside the OTP release, such as frontend assets or generated config. Configure lifecycle steps and still use the same ReleaseKit task:
config :release_kit, :artifact,
steps: [
before_compile: [],
before_release: [{MyApp.ReleaseStep, mode: :prod}],
after_release: [],
before_package: [],
after_package: []
]
A lifecycle step is any module that implements the ReleaseKit.Step behaviour:
defmodule MyApp.ReleaseStep do
@behaviour ReleaseKit.Step
@impl true
def run(opts) do
# build generated files before mix release runs
:ok
end
end
ReleaseKit.Step.Volt is compiled only when both optional dependencies :volt
and :npm are available in the consuming project. It installs locked npm
packages from the configured asset root, removes Volt's output directory, and
runs mix volt.build before the OTP release is assembled. It supports Volt
named profiles, --tailwind, source map mode, public directory, asset URL
prefix, and the production build toggles exposed by Volt's Mix task.
See examples/vanilla for a minimal Phoenix/Volt app that builds with plain:
MIX_ENV=prod mix release_kit.artifact --out-dir _build/prod/artifacts
Manifest shape
The manifest is an ETF-encoded %ReleaseKit.Manifest{} struct:
%ReleaseKit.Manifest{
tool: "release_kit",
format: :beam_release_artifact,
format_version: 2,
app: "my_app",
release: "my_app",
version: "20260620-abcdef0",
mix_env: "prod",
tarball: "/absolute/path/to/my_app-20260620-abcdef0.tar.gz",
artifact: %ReleaseKit.ArtifactInfo{
path: "/absolute/path/to/my_app-20260620-abcdef0.tar.gz",
size: 1_234_567,
compression: :gzip,
checksum: %{
algorithm: :sha256,
value: "...",
sidecar: "/absolute/path/to/my_app-20260620-abcdef0.tar.gz.sha256"
}
},
target: %ReleaseKit.Target{os: "linux", arch: "x86_64", libc: "glibc"},
build: %ReleaseKit.BuildInfo{
release_kit_version: "0.2.0",
elixir_version: "1.20.0",
otp_version: "28",
git_revision: "abcdef0",
git_dirty?: false
},
assets: [
%ReleaseKit.AssetInfo{
tool: :volt,
profile: :my_app_web,
outdir: "priv/static/assets",
manifest: "priv/static/assets/manifest.json",
asset_url_prefix: "/assets",
tailwind?: true
}
],
runtime: %{command: ["bin/my_app", "start"]},
env: %{
clear: %{"MY_APP_WEB" => "true"},
secret: []
},
health_check: %{
path: "/",
port: 4000,
url: "http://127.0.0.1:4000/"
}
}
Task options
--out-dir PATH Directory for the tarball and manifest
--release NAME Mix release name; defaults to the app name
--version VERSION Artifact version; defaults to YYYYMMDD-gitsha
--port PORT HTTP port recorded in health-check metadata
--health-path PATH HTTP path recorded in health-check metadata
--skip-release Package an existing _build/.../rel release directory
--target-os OS Override the manifest target OS
--target-arch ARCH Override the manifest target architecture
--target-libc LIBC Override the manifest target libc
--target-suffix Include the target in artifact and manifest filenames
Deployment tool contract
ReleaseKit intentionally stops after producing files. A deployment system should:
- read the ETF manifest with
:erlang.binary_to_term/1; - verify
artifact.checksumagainst the tarball or.sha256sidecar; - unpack the tarball into the deployment-specific release directory;
- materialize
env.clearand resolve secret names fromenv.secret; - start
runtime.commandfrom the unpacked release root; - supervise the release with the host's process manager;
- poll
health_check.urlwhen present before promoting traffic.
Manifest fields are additive. Consumers should ignore unknown keys whose
format_version they support.