A service is a named, long-running process inside a box, supervised by the box’s process supervisor. zomg exec --bg gives you a raw detached process keyed only by its PID, with no supervision. A service wraps a command in a managed lifecycle: a stable name, a desired-state spec, a state machine, an optional readiness gate, a restart policy, durable logs, and an optional public URL. Use a service for anything that should keep running — a web server, an API, a background worker — and come back on its own after a crash or a zomg stop/zomg resume. For one-off or short scripts, plain zomg exec is enough.

Mental model

  • A service is identified by its box and a service name, both of which must be valid names (lowercase a-z0-9-). You manage services with zomg service subcommands.
  • Every box runs a supervisor as PID 1: tini execs the per-box supervisor binary at /usr/local/lib/zomg/box-supervisor (shipped in the API image and injected into each box root, never a shared volume). The supervisor owns each service’s process, restarts, readiness, and logs. Services do not get their own DNS name — you reach them through the box.
  • A service’s desired spec lives at /var/lib/boxes/services/<service>.json, written and owned solely by the supervisor. It records the command, port, user, env, ready check, stop policy, restart policy, an optional working directory (cwd, settable via the API/pack path — the CLI does not expose it yet), and whether the service should be running. port is optional at the API level: a portless service has no derived URL and cannot be published (400 port_required); the CLI keeps --port required.
  • A service’s log is /var/lib/boxes/services/<service>.log, capped at 10 MiB. It survives zomg stop/zomg resume, is appended across restarts, and is readable even while the service is stopped. An oversized log is rotated to <service>.log.1 at spawn (a plain rename); while a service is running, a housekeeping pass every 60 s copy-truncates instead — copy to <service>.log.1, then truncate in place so the child’s open append fd stays valid (a few lines written in the copy window can be lost). Both the spec and the log are destroyed by zomg delete.
  • A publish maps a name to one (box, port) pair and attaches an ingress host so the port is reachable at a public URL. Publishing and unpublishing only touch ingress; the process keeps running. A service stores no publish state — its url is derived live from the box’s publishes (the publish whose port matches the service’s port).
  • Blue/green is ordinary publishes: run the new version as a second service, publish it under its own name to test, then repoint the stable name onto the new port.

State machine

service status, service inspect, and service list report one of seven states:
StateMeaning
startingSpawned; the readiness probe has not passed yet.
runningAlive, with no readiness check configured.
readyAlive and the readiness check passed.
backoffExited; a restart is scheduled after a backoff delay.
exitedRan to completion and the restart policy keeps it down after a clean (zero) exit (restart: never or on-failure).
failedExited nonzero and restart: never keeps it down; also the state of a corrupt-spec tombstone (with an error set).
stoppedDesired-stopped (via zomg service stop).
A service name with no spec returns 404 service_not_found. A spec file that fails to parse is not silently dropped: it shows in service list/status as a failed tombstone carrying the parse error, so you can zomg service remove it and re-create.
The reproducible command hints returned by service status --json and service inspect are generated by the API and begin with the literal word box (for example box service start ...). The actual CLI binary is zomg. Substitute zomg for box when copying these hints.

Service vs exec --bg

zomg exec my-box --bg -- python worker.py
zomg exec --bg runs a command detached and returns its PID. That is all you get: no name, status, log tailing, restart, or publish — only a PID, and it stays dead once it exits or the pod restarts. zomg service start hands the command to the supervisor instead, which adds a stable name, a desired-state spec, readiness gating, a restart policy, durable logs, status inspection, stop, and optional publishing — and brings the service back after a crash or a zomg stop/zomg resume. Reach for a service when you need any of that.

Start a service

zomg service start my-box web --port 8080 -- node server.js
The positional arguments are the box and the service name. The command to run goes after --. Flags:
  • --port <PORT>required. The port the process listens on inside the box.
  • --publish <NAME> — optional. Publish the service at this name under the Zomg domain. Published URLs are gated by deployment auth by default; add --public to skip the gate.
  • --public — optional. Make the published URL public (skip deployment auth). See Publish and unpublish.
  • --user <USER> — optional. Run the process as this box-local user — a user that exists inside the box, not a deployment user.
  • -e, --env <KEY=VALUE> — repeatable. Service-only environment, exported to the process. These override values from /etc/boxes-env.
  • --ready <SPEC> — optional readiness gate: tcp (probe the service port), tcp:PORT, or http:PATH. start blocks until the service is ready or the timeout elapses.
  • --ready-status <CODE> — expected HTTP status for an http: ready check (default 200).
  • --ready-timeout <SECS> — readiness timeout (default 60, capped at 240).
  • --restart <POLICY> — restart policy: always (default), on-failure, or never. Restarts back off exponentially from 1s to 30s; a restarts counter tracks how many times the supervisor has respawned the service.
  • --stop-signal <SIGNAL> — signal sent to stop the process (default TERM).
  • --stop-grace <SECS> — seconds to wait after the stop signal before the supervisor SIGKILLs the whole process group (default 10).
  • -t, --tag <K=V> — repeatable. Attach tags to the service.
  • -p, --project <P>
zomg service start my-box web --port 8080 --publish app -t box.ui=main -- node server.js
The -t box.ui=main tag is suggested in the CLI help as a marker for the primary web UI. It is a labeling convention only — no behavior is attached to it.
The command is required. Starting a service with no command after -- fails with Error: command is required and exits 1. The API also rejects an empty command and validates both the box name and service name — an invalid name returns invalid_name; an invalid restart policy or a ready check that sets neither or both of tcp/http returns 400. The supervisor writes the spec, spawns the process in its own session and process group (as --user if given, with --env exported over /etc/boxes-env), and runs any readiness probe. Because start blocks until the service leaves starting, it returns once the service is running/ready or has failed — a failed start returns 502 service_start_failed and leaves the service under supervision, so remove it with zomg service remove. An imperative service start on a service that is already alive returns 409 service_already_running — restart or stop it instead. (The pack path applies services idempotently, so re-running instance-create converges rather than 409ing.) If --publish was given, publishing happens after the service starts.

Inspect services

Status

zomg service status my-box web
Prints the state, tags (sorted, comma-joined), user, restart policy, restart count, last exit code, PID, port, and URL (whichever apply). Pass -j, --json for the raw payload.

Inspect

zomg service inspect my-box web
inspect adds the command on top of status, followed by reproducible command hints for start, publish, and unpublish. Pass -j, --json for the raw payload.

List

zomg service list my-box
Lists every service in the box as a table with columns NAME, STATE, PID, PORT, RESTARTS, TAGS, URL. The URL column is filled in when a publish routes to the service’s port. Pass -j, --json for raw output.

Logs

zomg service logs my-box web --lines 200
Tails the service’s durable log at /var/lib/boxes/services/<service>.log. -l, --lines defaults to 100. The log is readable even while the service is stopped (it survives stop/resume and is appended across restarts); a service that has no spec returns 404 service_not_found.
service logs has no --follow and no --json. It returns a single tail of the log.

Restart and stop

Restart

zomg service restart my-box web
Asks the supervisor to stop the process and start it again from the same spec, resetting the restarts counter. Like start, it blocks until the service is ready or has failed. Restarting a stopped service starts it again.

Stop

zomg service stop my-box web
Stop asks the supervisor to send the stop signal (--stop-signal, default TERM) to the process group, wait --stop-grace seconds (default 10, capped at 60 — a larger value returns 400 invalid_stop_grace), then SIGKILL the group if it is still alive. It records the desired state as stopped before the kill, so a supervisor crash mid-grace cannot resurrect the service on the next boot, and removes every publish that routes to the service’s port. The spec and log remain, so the service shows as stopped, its logs stay readable, and it no longer has a published URL.

Remove

zomg service remove my-box web
Remove stops the service with grace (if it is alive), tears down its publish, then deletes the spec so it no longer reconciles at boot. The log file is kept. Use it to retire a service permanently, or to clear a corrupt-spec tombstone.

Reconciliation across stop/resume

zomg stop tears the pod down and zomg resume starts a fresh one, so every in-box process — including your services — is killed. At boot the supervisor reads every spec under /var/lib/boxes/services/ and brings back each service whose desired state is running, re-running its readiness probe and recovering its publishes. A service you stopped with zomg service stop has desired state stopped and stays down until you start it again — as does a service that reached a terminal exited/failed state, so a run-once job is not respawned by a box resume. (The terminal park is persisted as desired-stopped, so after a resume such a service reports stopped; the point is that it stays down.)

Publish and unpublish

A publish maps a name to one (box, port) pair and attaches an ingress host, so the service’s port is reachable at a public URL. The process keeps running throughout — publishing only changes ingress. By default a published service URL is gated by deployment auth: visitors sign in through the deployment’s auth provider before they reach it. Pass --public to skip the gate and expose the URL to anyone who has it. On a deployment with no auth provider configured, publishes cannot be gated — they are public and the CLI prints a warning. See Auth for the full model.
zomg service publish my-box web --as app
Flags:
  • --as <NAME>required. Name under the Zomg domain.
  • --port <PORT> — optional. Defaults to the service’s port. Override it to point the name at a different port during a blue/green cutover.
  • --public — optional. Skip deployment auth on this publish.
  • -j, --json
On success the CLI prints the public URL and a visibility: line (public or gated (deployment auth)), plus any unconfigured-auth warning on stderr. With --json it prints the publish record: {"host":"app.example.com","url":"https://app.example.com","port":8080,"public":false} (a "warning" field is added only when the publish is public solely because no auth provider is configured). URLs are built as https://<name>.zomg.ai using your instance’s domain. Apex and custom-domain service publishing are not part of zomg service publish. Publishing a name that already exists on the same box repoints it: a name maps to exactly one (box, port), so republishing with a new --port moves traffic in a single ingress update. A name owned by a different box fails with host_already_in_use; take it over with the box-level zomg publish <box> --port <port> --as <name> --force.
zomg service unpublish my-box web
Unpublish removes every publish that routes to the service’s port (--json prints the removed hosts: {"hosts":["app.example.com"]}). The process keeps running. To retire one name and keep others, use the box-level zomg unpublish <box> <name>.

Blue/green deploys

A name points at one (box, port). To preview a new version, publish it under its own name — on the same box or a fresh one. To promote, publish the new backend under the stable name. To roll back, point the name back. Never construct URLs from naming conventions; generated names are only knowable from command output or --json (url).
1

Run the candidate as a second service

zomg service start my-box web-next --port 8081 -- node server.js
The current web service keeps serving https://app.zomg.ai untouched.
2

Publish the candidate under a preview name

zomg service publish my-box web-next --as app-preview
Test the new version at the URL in the response. To use a generated name instead, publish the port directly with zomg publish my-box --port 8081 and read the URL from its output — never guess it.
3

Promote by repointing the stable name

zomg service publish my-box web-next --as app --port 8081
app now routes to the candidate’s port. The switch is one ingress update, with no gap. To roll back, point the name at the old port again: zomg service publish my-box web --as app --port 8080.
4

Retire the old version

zomg unpublish my-box app-preview
zomg service stop my-box web
Remove the preview name with the box-level zomg unpublish — the stable name now shares the candidate’s port, so zomg service unpublish my-box web-next would remove both. Stop the old service once you no longer need the rollback path.

Connecting services

Services do not get their own DNS names. A service listens on a port on its box’s pod, so you reach it through the box’s name and port. Pack instances form their internal host as <box>.<project>. A Postgres box orders-db in project shop is reachable at orders-db.shop:5432; a Redis box cache in the same project is cache.shop:6379; a Hermes Agent box agent exposes the API at agent.shop:8642 and its Desktop backend at agent.shop:9119. Use these <box>.<project> hosts (as exposed by a pack’s computed outputs — see zomg outputs) to wire services to their dependencies.

Boxes

The Kubernetes-backed sandbox a service runs inside.

Packs

Declare a service (base, env, supervised processes, verbs) as data and instantiate it many times.

Environment

The runtime, filesystem, and DNS your services see inside a box.

Data volumes

Persistent storage that outlives a box — where database state lives.

API reference

The HTTP routes behind zomg service.