No shell boilerplate
One Go binary + a declarative JS script. Readable, testable, and no shell dependencies.
INIT› ezx · v0.8.1 · Apache 2.0
Replace fragile bash entrypoint.sh trees with a single Go binary driven by a declarative JavaScript script. Supervision, readiness, scheduling, and secrets — baked into your image, without a line of shell.
› FEATURES
01 / FEATURESOne Go binary + a declarative JS script. Readable, testable, and no shell dependencies.
INITFlat nodes + dependsOn edges: fan-in, shared dependencies, and diamonds — no nested tree required.
DAGdependsOnEdges picks started, ready, or exit per dependency — wait for one dep to be ready while another only needs to start.
DAGoneshot: true runs a process to completion; dependents start after it exits 0. Retryable via restart.
ONESHOTRoute a node's stdout/stderr to its own rotating file: log.stdout: "file", filePath, maxBytes, maxBackups.
LOGEnvironment variables map to config files and CLI flags declaratively. Same env, same output, every time.
ENVfilterEnv / filterEnvPattern strips sensitive vars before spawning any child.
FILTERBuilt-in zombie reaping, signal forwarding, and graceful drain — or hand PID 1 to the app if you prefer.
INITChoose exec mode (app is PID 1) or supervised mode (ezx stays PID 1). Your container, your call.
EXEC/readyz, /livez, /healthz, and cron-driven scheduler with readiness gates are built in.
SCHEDULERExpose user-defined HTTP routes on the shared health server for manual triggers and runtime ops.
APIRender key=value, table/matrix, INI, and deterministic YAML from structured objects.
FILES› CODE EXAMPLES
02 / CODEconst { chain } = require("ezx");
// POSTGRESQL_CONFIG_LOG_DESTINATION=stderr
// becomes log_destination = 'stderr' in postgresql.conf
chain.run({
nodes: [{
name: "postgres",
files: [{
path: "/etc/postgresql/postgresql.conf",
operations: [{
type: "set-property",
fromEnvPattern: "^POSTGRESQL_CONFIG_(.+)$",
nameTransform: "lower",
pattern: "^\\s*#?\\s*${name}\\s*=.*",
value: "${name} = ${value}",
valueFormat: "auto",
}],
}],
// files are provisioned before the process starts
process: {
binaryPath: "postgres",
arguments: ["-D", "/var/lib/postgresql/data"],
},
}],
});
const { chain } = require("ezx");
// PROMETHEUS_ENABLE_WEB_LIFECYCLE=true
// THANOS_QUERY_STORE_ADDRESSES=store1:10901,store2:10901
// becomes: --web.enable-lifecycle --endpoint=store1:10901 --endpoint=store2:10901
chain.run({
nodes: [{
name: "thanos",
process: {
binaryPath: "thanos",
argOperations: [
{ when: { name: "PROMETHEUS_ENABLE_WEB_LIFECYCLE", value: "true" }, flag: "--web.enable-lifecycle", format: "bare-flag" },
{ flag: "--endpoint", fromEnv: "THANOS_QUERY_STORE_ADDRESSES", split: "," },
],
},
}],
});
const { chain } = require("ezx");
// Flat nodes + dependsOn edges. postgres is a shared dependency; backup is
// fan-in — it waits for BOTH pgbouncer AND pgbackrest to be ready.
chain.run({
nodes: [
{ name: "postgres", process: { binaryPath: "postgres", arguments: ["-D", "/var/lib/postgresql/data"] } },
{ name: "pgbouncer", dependsOn: ["postgres"], needParentReady: true,
process: { binaryPath: "pgbouncer", arguments: ["/etc/pgbouncer/pgbouncer.ini"] } },
{ name: "pgbackrest", dependsOn: ["postgres"], needParentReady: true,
process: { binaryPath: "pgbackrest", arguments: ["--config=/etc/pgbackrest.conf", "backup"] } },
{ name: "backup", dependsOn: ["pgbouncer", "pgbackrest"], needParentReady: true,
process: { binaryPath: "pgbackrest", arguments: ["--config=/etc/pgbackrest.conf", "backup"] } },
],
});
const { chain } = require("ezx");
chain.run({
nodes: [{
name: "pgbackrest",
process: {
binaryPath: "pgbackrest",
arguments: ["--config=/etc/pgbackrest.conf", "backup"],
// Strip PGBACKREST_* from the child env so they do not leak or conflict.
filterEnvPattern: ["^PGBACKREST_"],
},
}],
});
const { chain, scheduler } = require("ezx");
chain.run({
nodes: [
{ name: "postgres", process: { binaryPath: "postgres", arguments: ["-D", "/var/lib/postgresql/data"] } },
{
name: "hourly-backup",
dependsOn: ["postgres"],
needParentReady: true,
process: { binaryPath: "pgbackrest", arguments: ["--config=/etc/pgbackrest.conf", "backup"] },
scheduler: scheduler.build({
schedule: { expression: "0 * * * *", timezone: "UTC" },
minInterval: 60e9, // dedupe: never run twice within 60s
gate: { type: "exec", exec: ["pg_isready", "-h", "/run/postgresql"] },
}),
},
],
});
const { chain } = require("ezx");
const PGDATA = "/var/lib/postgresql/data";
// Init steps (oneshot) run to completion; only when they exit 0 does the
// main app become PID 1 via exec — the "init DAG → exec main" pattern.
chain.run({
nodes: [
{
name: "initdb",
oneshot: true,
process: { binaryPath: "/usr/local/bin/initdb", arguments: ["--username=postgres", "--pwfile=/tmp/ezx-pwfile", PGDATA], user: "postgres", group: "postgres" },
},
{
name: "stanza-create",
oneshot: true,
dependsOn: ["initdb"],
process: { binaryPath: "/usr/bin/pgbackrest", arguments: ["stanza-create", "--stanza=main", "--pg1-path=" + PGDATA] },
},
{
name: "postgres",
exec: true,
dependsOn: ["initdb", "stanza-create"],
process: { binaryPath: "/usr/local/bin/postgres", arguments: ["-D", PGDATA], user: "postgres", group: "postgres" },
},
],
});
› HOW IT WORKS
03 / HOW IT WORKSezx loads a JavaScript entrypoint script against its Go-hosted JS engine (goja). The script declaratively describes everything: which files to provision, how to build CLI arguments from env, which processes to spawn, how they depend on each other, and how to supervise them.
The core abstraction is the dependency graph — a ProcessNode connected by explicit dependsOn edges (the flat `nodes`/`dependsOn` form). A node starts after all of its dependencies have started; per-edge wait modes let each dependency independently gate on started, ready, or exit; oneshot nodes run to completion and act as success gates; siblings / independent nodes start in parallel; and any node can carry a restart policy, a health server, a scheduler (cron), forwardSignals, a probe, and graceful shutdown config.
› LAUNCH MODES
04 / MODESezx does the setup (env templating, file provisioning, spawning support processes), then syscall.Exec the app. The app becomes PID 1 and handles its own signals natively. Zero supervision overhead — but no restart/health-check safety net. Must be a leaf node and is mutually exclusive with health.
ezx stays PID 1. Each node gets restart, health/readiness HTTP, scheduler, forwardSignals, probe, shutdown, and dependency edges via dependsOn / dependsOnEdges. ezx reaps zombies and forwards signals. Declare one of those (or execDefault: false) — a lone node with none of them is auto-exec'd instead.
An alternative to tini and dumb-init that goes further — or hand PID 1 to your app with exec mode for native signals.
› INSTALL · v0.8.1
05 / INSTALLPrebuilt binaries are on the GitHub releases page, or build from source.
curl -LO https://github.com/supanadit/ezx/releases/download/0.8.1/ezx-0.8.1-linux-amd64 && mv ezx-0.8.1-linux-amd64 ezx && chmod +x ezx
curl -LO https://github.com/supanadit/ezx/releases/download/0.8.1/ezx-0.8.1-linux-arm64 && mv ezx-0.8.1-linux-arm64 ezx && chmod +x ezx
curl -LO https://github.com/supanadit/ezx/releases/download/0.8.1/ezx-0.8.1-darwin-amd64 && mv ezx-0.8.1-darwin-amd64 ezx && chmod +x ezx
curl -LO https://github.com/supanadit/ezx/releases/download/0.8.1/ezx-0.8.1-darwin-arm64 && mv ezx-0.8.1-darwin-arm64 ezx && chmod +x ezx
go build -o ezx ./app
Build a single static binary from source with the standard Go toolchain.
Read env, provision files, and run a tiny process chain — the fastest way to see ezx in action.
./ezx bootstrap examples/bootstrap/demo.js
The same flow inside a real image — mount the binary and the examples into postgres:16 and bootstrap from /examples.
docker run --rm -e POSTGRES_PASSWORD=secret \
-v "$PWD/ezx:/usr/local/bin/ezx" \
-v "$PWD/examples:/examples" postgres:16 \
ezx bootstrap /examples/bootstrap/postgres.js
› FAQ
06 / FAQezx is a deterministic, scriptable container entrypoint engine: a single Go binary driven by a declarative JavaScript script that replaces fragile bash entrypoint trees with supervision, readiness, scheduling, and secrets handling.
No. Everything is declared in a JavaScript entrypoint script — file provisioning, env-driven config and CLI args, dependency graphs, health checks, scheduling, and secrets filtering. No sed/awk, sleep polling, or pgrep loops.
Yes — with Mode A (exec) ezx does the setup then execs the app so it becomes PID 1 and handles signals natively. With Mode B (supervised) ezx stays PID 1 for supervision, health, scheduling, and dependency ordering — declare restart/health/scheduler, or execDefault: false, because a lone node with none of them is auto-exec'd. As an alternative to tini and dumb-init, ezx covers the init duties you expect from them and goes further.
ezx hosts its JavaScript engine in-process via goja (a pure-Go ES5.1/ES6 interpreter), so there is no external Node dependency in the container.
Apache License 2.0. The current release is v0.8.1; prebuilt binaries are available on the GitHub releases page, or you can build from source.
Replace fragile shell scaffolding with a deterministic, scriptable control plane in Go.
Get started →