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 withzomg servicesubcommands. - Every box runs a supervisor as PID 1:
tiniexecs 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.portis optional at the API level: a portless service has no derived URL and cannot be published (400 port_required); the CLI keeps--portrequired. - A service’s log is
/var/lib/boxes/services/<service>.log, capped at 10 MiB. It surviveszomg stop/zomg resume, is appended across restarts, and is readable even while the service is stopped. An oversized log is rotated to<service>.log.1at 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 byzomg 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
urlis 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:
| State | Meaning |
|---|---|
starting | Spawned; the readiness probe has not passed yet. |
running | Alive, with no readiness check configured. |
ready | Alive and the readiness check passed. |
backoff | Exited; a restart is scheduled after a backoff delay. |
exited | Ran to completion and the restart policy keeps it down after a clean (zero) exit (restart: never or on-failure). |
failed | Exited nonzero and restart: never keeps it down; also the state of a corrupt-spec tombstone (with an error set). |
stopped | Desired-stopped (via zomg service stop). |
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.
Service vs exec --bg
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
--.
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--publicto 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, orhttp:PATH.startblocks until the service is ready or the timeout elapses.--ready-status <CODE>— expected HTTP status for anhttp:ready check (default200).--ready-timeout <SECS>— readiness timeout (default60, capped at240).--restart <POLICY>— restart policy:always(default),on-failure, ornever. Restarts back off exponentially from 1s to 30s; arestartscounter tracks how many times the supervisor has respawned the service.--stop-signal <SIGNAL>— signal sent to stop the process (defaultTERM).--stop-grace <SECS>— seconds to wait after the stop signal before the supervisor SIGKILLs the whole process group (default10).-t, --tag <K=V>— repeatable. Attach tags to the service.-p, --project <P>
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.-- 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
-j, --json for the raw payload.
Inspect
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
-j, --json for raw output.
Logs
/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
restarts counter. Like start, it blocks until the service is ready or has failed. Restarting a stopped service starts it again.
Stop
--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
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.
--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
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.
--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).
Run the candidate as a second service
web service keeps serving https://app.zomg.ai untouched.Publish the candidate under a preview name
zomg publish my-box --port 8081 and read the URL from its output — never guess it.Promote by repointing the stable name
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.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.
Related
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.