› ezx · v0.8.1 · Apache 2.0

Your container's control plane, inside the image.

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.

$ ./ezx bootstrap examples/bootstrap/demo.js

› FEATURES

01 / FEATURES

Every primitive your container needs.

01

No shell boilerplate

One Go binary + a declarative JS script. Readable, testable, and no shell dependencies.

INIT
02

Dependency graph (DAG)

Flat nodes + dependsOn edges: fan-in, shared dependencies, and diamonds — no nested tree required.

DAG
03

Per-edge wait modes

dependsOnEdges picks started, ready, or exit per dependency — wait for one dep to be ready while another only needs to start.

DAG
04

Oneshot services

oneshot: true runs a process to completion; dependents start after it exits 0. Retryable via restart.

ONESHOT
05

Per-service logging with rotation

Route a node's stdout/stderr to its own rotating file: log.stdout: "file", filePath, maxBytes, maxBackups.

LOG
06

Env-driven determinism

Environment variables map to config files and CLI flags declaratively. Same env, same output, every time.

ENV
07

Secret filtering

filterEnv / filterEnvPattern strips sensitive vars before spawning any child.

FILTER
08

PID-1 init duties

Built-in zombie reaping, signal forwarding, and graceful drain — or hand PID 1 to the app if you prefer.

INIT
09

Two launch modes

Choose exec mode (app is PID 1) or supervised mode (ezx stays PID 1). Your container, your call.

EXEC
10

Health & scheduling

/readyz, /livez, /healthz, and cron-driven scheduler with readiness gates are built in.

SCHEDULER
11

Runtime API

Expose user-defined HTTP routes on the shared health server for manual triggers and runtime ops.

API
12

Declarative builders

Render key=value, table/matrix, INI, and deterministic YAML from structured objects.

FILES

› CODE EXAMPLES

02 / CODE

The same operations you currently write in bash — as a readable script.

const { 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 WORKS

A declarative script. A Go-hosted JS engine. A dependency graph.

ezx 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.

[01] Script written
[02] Loaded by goja
[03] Graph built
[04] Nodes scheduled
[05] Supervised by ezx (PID 1)

› LAUNCH MODES

04 / MODES

Two ways to run your container.

Mode A — exec

ezx 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.

Mode B — supervised

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 / INSTALL

Single static binary. No dependencies.

Prebuilt 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.

sh
./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.

sh
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 / FAQ

Questions, answered.

What exactly is ezx?

ezx 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.

Do I need to write shell anymore?

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.

Can my app still be PID 1?

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.

What runtime does the script use?

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.

What licence is ezx under?

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.

Take control of your entrypoint.

Replace fragile shell scaffolding with a deterministic, scriptable control plane in Go.

Get started →