A box is a full Linux environment you can exec into. It boots from a base image and keeps its filesystem across stop and resume. Under the hood it’s a single-replica Kubernetes Deployment with a headless Service and an Ingress, backed by a copy-on-write btrfs root filesystem. You manage boxes through the zomg CLI. Every command on this page targets a box by its ref. Read reference resolution first if a command can’t find your box.

Projects and reference resolution

Every box lives in a project. A box’s identity is the pair (project, name), so the same name can exist in different projects.

Project resolution

When you don’t pass --project, the CLI resolves the project in this order:
  1. The --project / -p flag.
  2. The ZOMG_PROJECT environment variable.
  3. The git repository root directory name, if it’s a valid project name.
  4. "default".
Run zomg project current to print the resolved project.
zomg project current

Box ref forms

A box ref accepts several forms:
FormMeaning
nameBox name in the default project.
project:nameCanonical explicit project (colon).
project.nameSame, dot sugar (colon is tried first, then dot).
.The current box, from ZOMG_PROJECT + BOX_NAME (errors if unset).
-An ephemeral box (see Ephemeral boxes).

Naming rules

  • Project: 1–63 characters, a-z, 0-9, -; starts and ends alphanumeric; no --.
  • Box name: a DNS label — 1–63 characters, a-z, 0-9, -; starts and ends alphanumeric.
  • Tag keys and values: may also use _ and ..

Target selection and profiles

The CLI needs a base URL and a token to reach the API. The URL resolves in this priority:
  1. The --url flag.
  2. The selected profile (the --profile flag or ZOMG_PROFILE, plus ZOMG_PROFILE_<NAME>_URL).
  3. ZOMG_URL.
  4. ZOMG_API_URL.
Most of the time you don’t pass any of these. zomg config set profile <name> stores a default profile and every command targets it. Use the global --profile <name> flag to target a different profile for a single command without changing the default. --profile works on any command and is equivalent to setting ZOMG_PROFILE for that invocation. A profile is selected by name. Profile variables are named ZOMG_PROFILE_<UPPERCASED_NAME>_<SUFFIX>, where any non-alphanumeric character in the name becomes _. The suffixes include URL, TOKEN, SSH_HOST, INSECURE, and IP.
export ZOMG_PROFILE=staging
export ZOMG_PROFILE_STAGING_URL=https://api.zomg.example.com
export ZOMG_PROFILE_STAGING_TOKEN=...
zomg list
If a profile is selected but its _URL is unset, the CLI errors:
Error: profile "staging" is selected but ZOMG_PROFILE_STAGING_URL is not set
Local setup config lives in .zomg/config.yaml (or $ZOMG_SETUP_DIR/config.yaml) and is read and written by zomg config set/get/list/unset/path/profiles. When ZOMG_PROFILE matches the config’s profile/env key, the CLI auto-derives api_url (or https://api.<zomg_domain>), ssh_host (or ssh.<zomg_domain>), and the token.

Create a box

zomg create web-1 --base box-base --cpu 2 --memory 2G
zomg create <name> provisions a new box. On success it prints OK, or raw JSON with --json.
FlagPurpose
--base, -b <BASE>Base image to create from. Defaults to box-base.
--fork, -f <BOX>Clone another box’s filesystem instead. Mutually exclusive with --base.
--tag, -t key=valueAttach a tag (repeatable). Used by list -t filtering.
--data, -d NAME=/mount/path[:ro|:rw]Attach a data volume (repeatable, default :rw).
--env, -e KEY=VALUESet a container environment variable (repeatable).
--cpu <CPU>CPU request, e.g. 2 or 500m.
--memory <MEM>Memory request, e.g. 2G or 512M.
--project, -p <PROJECT>Target project.
--json, -jJSON output.

Base vs clone

Pass --base to start from a template image, or --fork to clone an existing box. You can’t use both:
Error: --base cannot be used with --fork
A --fork clone snapshots the source root and each non-system data volume into generated box-owned volumes named like box-<box-prefix>-<12 hex>. Those generated volumes are attached to the new box, hidden from normal attachment by ownership metadata, and deleted with the owning box after deletion fully completes. The clone inherits the source’s base, config, tags, env (merged with your overrides), port, and termination grace. You can’t clone a box from itself. Tagged base builds resolve through the alias:tag form:
zomg create db-1 --base box-base-postgres-17:0123abcd
See Snapshots & bases for building and tagging bases.

Data volumes

--data attaches a data volume at a mount path. The mount path must be absolute and cannot be / or sit under /proc, /sys, /dev, /run, /etc, /volumes, /home, or /box. The data volume must be in the same project as the box, and each mount name must be unique.
zomg create web-1 --data assets=/srv/assets:ro --data uploads=/srv/uploads
See Data volumes for managing volumes, attachments, and backups.
Reserved environment keys are rejected at create time: ZOMG_PROJECT, BOX_NAME, ZOMG_API_URL, ZOMG_API_HOST, and TERM.

List, show, update

list

zomg list
zomg list --all --tag team=web
zomg list (alias ls) prints a table with NAME STATUS BASE URL PUBLISHED columns. The PUBLISHED column shows https://<host> entries joined by commas.
FlagPurpose
--all, -aList across all projects (adds a PROJECT column).
--quiet, -qNames only; with --all, prints project:name.
--tag, -t key=valueFilter by tag (repeatable, AND).
--project, -p / --json, -jProject / JSON output.

show

zomg show web-1
zomg show <box> prints details: name, project, status, base, url, port, timestamps, published hosts, tags, data mounts, and (from config) cpus, ram_mb, and storage_gb.

update — what it actually changes

zomg update web-1 PORT=8080 CPU=4 MEMORY=4G
zomg update <box> [SETTINGS...] takes positional KEY=VALUE settings plus --env, --tag, -p, and -j. The CLI merges all settings into the environment map and sends them.
The server acts only on a fixed allowlist. Everything else you pass is silently dropped.
The server applies, from the settings you send:
  • PORT — updates the Service/Ingress port (1–65535).
  • CPU — updates the CPU resource.
  • MEMORY — updates the memory resource.
  • TERMINATION_GRACE_PERIOD_SECONDS or TERMINATION_GRACE_SECONDS — up to 86400.
  • Tags.
Every other KEY=VALUE is parsed but never applied — there is no path to mutate container environment variables after create. If nothing in the allowlist is present, the server returns 400 no_updates ("no updates provided").
To change a box’s container environment after creation, recreate the box — optionally with --fork to keep the existing root filesystem.

Stop, resume, delete

stop and resume

zomg stop web-1
zomg resume web-1
zomg stop tears down the pod but preserves the filesystem. zomg resume recreates the pod and re-injects environment variables. Both print OK.
The pod IP changes on stop/resume because a box is single-replica. Clients reaching a box over inter-box DNS should reconnect after a resume.

delete

zomg delete web-1 web-2
zomg delete web-1 --force
zomg delete <box...> (alias rm) deletes one or more boxes. Per box it prints <name>: deleted or <name>: error: <msg>; any error exits non-zero.
FlagPurpose
--force, -fDelete even if the box is running.
--waitPoll until API cleanup fully completes instead of accepting an async delete.
--wait-timeout <SECONDS>Maximum wait time with --wait. Defaults to 240 seconds.
--project, -p / --json, -jProject / JSON output.
Without --force, deleting a running box returns 409 box_running ("box is running"). Delete is idempotent. By default, if Kubernetes or filesystem cleanup is accepted but not fully complete, the API returns 202 and the CLI prints <name>: deleting while a background retry continues. Pass --wait when the caller needs a terminal result before continuing. The CLI polls GET /v1/projects/{project}/boxes/{name}/delete-status until the status is deleted or failed, or until --wait-timeout expires. Delete status values are:
StatusMeaning
deletingCleanup is still in progress or retrying.
deletedCleanup finished, or no box by that name exists.
failedBackground retry exhausted and manual repair may be needed.
activeThe box exists and is not currently being deleted.

Publish a custom hostname

zomg publish web-1 app
zomg publish web-1 @
zomg publish web-1 --host app.example.com
zomg publish <box> [subdomain] exposes the box on a custom hostname with TLS. unpublish removes it.
FlagPurpose
subdomainRequired unless --host. @ means the apex (bare domain).
--host <HOST>Explicit full hostname (conflicts with subdomain).
--domain <DOMAIN>Override the default publish domain (conflicts with --host).
--force, -fTake over a subdomain already published elsewhere.
--project, -p / --json, -jProject / JSON output.
The host resolves as: --host wins; otherwise {subdomain}.{domain} where the domain defaults to the server’s publish domain; @ resolves to the bare domain. On success the CLI prints OK (or {"host": ...}). TLS uses a wildcard secret if the cluster has one configured, otherwise cert-manager issues a per-host certificate via the cluster issuer. Publishing a host already in use returns 409 unless you pass --force. Unpublishing a host not owned by the box returns 403.
Box-level zomg publish has no --port flag. Port-bound publishing for long-running services is covered on the Services page.

Inter-box DNS

Boxes reach each other by name. The Service is headless (clusterIP: None): it exposes all ports and resolves DNS straight to the pod IP, so traffic is not load-balanced. From inside a box in project P:
You requestResolves to
<box>A box in the same project P.
<box>.<other-project>A box in another project.
<box>.defaultA box in the default project.
any dotted FQDNExternal DNS.
# from inside a box, reach a sibling box named "db" on port 5432
psql -h db -p 5432 -U postgres
Because a box is single-replica, its pod IP changes on stop/resume. Reconnect after the target box resumes.
More on the in-box runtime, PATH, and environment variables is on the Environment page.

Run commands with exec

zomg exec web-1 -- ls /srv
zomg exec web-1 --sh -- "cat /etc/os-release | head"
zomg exec web-1 --bg -- ./long-job.sh
zomg exec <box> [flags] -- <cmd...> runs a command in a box.
FlagPurpose
--user <NAME>Run as a project user.
--env KEY=VALUESet an environment variable (repeatable).
--wd <DIR> (alias --cwd)Working directory.
--sh (alias --bash)Treat the args as a shell string.
--bgRun detached in the background; returns a PID.
--project, -p / --json, -jProject / JSON output.
Foreground (default) runs an interactive exec session and preserves the command’s exit code. If you give no command and stdin isn’t a TTY (and you didn’t pass --bg), it defaults to bash. --json is rejected in foreground mode:
Error: --json is not supported in foreground mode
Background (--bg) starts a detached process and prints pid=<n>. Use - as the box name to run in an ephemeral box.

Inspect with ps and logs

zomg ps web-1
zomg logs web-1 --pid 4123 --follow
zomg ps <box> runs ps inside the box (plus any extra args after --). zomg logs <box> --pid <PID> (alias tail) reads a background process’s log. --pid is required:
FlagPurpose
--pid <PID>Process to read (required).
--lines, -l <N>Number of lines (default 100).
--follow, -fTail in follow mode. Cannot combine with --json.
--project, -p / --json, -jProject / JSON output.

Open a shell with console

zomg console web-1
zomg console - --command "python3"
zomg console <box> opens an interactive shell. Use - for an ephemeral box that’s deleted on exit.
FlagPurpose
--tty / --no-ttyForce or disable an interactive TTY.
--raw / --no-rawForce or disable local terminal raw mode.
--term <TERM>Terminal type (defaults to TERM or xterm-256color).
--user <NAME>Run as a project user.
--command <CMD>Run this instead of a login shell.
--wd <DIR> (alias --cwd)Working directory.
--log-console-debugPrint transport details to stderr.
--project, -pTarget project.
console has no --json.

SSH via the gateway

zomg ssh web-1
zomg ssh web-1 --info
zomg ssh web-1 -- -A
zomg ssh <box> connects through the SSH gateway. The username is <project>--<box>@<host>.
FlagPurpose
--host <HOST> (env ZOMG_SSH_HOST)Gateway host. Otherwise resolved from the profile’s _SSH_HOST or _URL host.
--port <PORT> (env ZOMG_SSH_PORT)Gateway port (default 2222).
--infoPrint the resolved ssh command and exit.
-- <ssh-args>Extra arguments passed to ssh.
On Unix, zomg ssh execs into the real ssh client. It runs without API config.

Ports

zomg port list web-1
zomg port wait web-1 8080 --timeout 30
zomg port <list|wait> (alias ports) inspects listening ports.
  • port list <box> (alias ls): --tcp, --udp (default both), -p, -j. Columns: PROTOCOL ADDRESS PORT.
  • port wait <box> <port>: --tcp (default) or --udp, --timeout <s> (default 60, 0 = forever), --interval <s> (default 1), -p, -j. Passing both --tcp and --udp, or --interval 0, errors.

Copy files

zomg cp ./app.tar web-1:/srv/app.tar
zomg get web-1:/var/log/app.log ./app.log
zomg put ./config.yaml web-1:/etc/app/config.yaml
zomg cp <src> <dest> transfers files. zomg get and zomg put are aliases. Exactly one side must be a box path written as box:/path (the :/ is required) — both-remote or neither-remote errors. Qualified refs work: project:box:/path, .:/path (current box), and -:/path (ephemeral). File permissions are preserved in both directions.
Directories aren’t supported yet:
Error: only files are supported (directories require -r, not yet supported)
Data-volume gotcha: zomg cp writes into the box root filesystem and the system volumes only. A path under a custom data-volume mount is not redirected — it lands in the root subvolume, where the live data mount shadows it at runtime. So zomg cp does not write into attached data volumes. To put files into a data volume, run zomg exec inside the box (for example, pipe through a shell command).

System volumes

Every box auto-mounts these volumes:
MountScope
/box/binGlobal, on PATH.
/box/allGlobal shared data.
/box/projPer-project; /box/proj/bin is on PATH.
/homePer-project user homes.
The in-box environment snapshot file is /etc/boxes-env. Injected variables include ZOMG_PROJECT, BOX_NAME, ZOMG_API_URL, ZOMG_API_HOST, plus node/host IPs and TERM. See Environment for the full runtime layout.

Project users

zomg user create deploy --shell /bin/bash
zomg user list
zomg user <list|create|update|delete> manages users within a project. Created users get a synthesized UID/GID and a home at /home/<user>.
  • user create <name> [--shell <SHELL>] — prints name<TAB>uid<TAB>home.
  • user list — prints name uid home, tab-separated.
  • user update <name> [--shell <SHELL>] — updates the shell.
  • user delete <name> — removes the user (no --json).
All except delete accept -p and -j. Reference a user from exec and console with --user.

Ephemeral boxes

Pass - as the box name to exec, console, or cp/get/put to auto-create a box, run, and auto-delete it.
zomg exec - -- python3 --version
zomg console -
The generated name is ephemeral-<pid>-<millis>, and the CLI prints ephemeral box: <name> to stderr. Use project:- to target a specific project. --json isn’t supported for ephemeral exec, and ephemeral deletes always force.

Inject credentials

zomg inject web-1
zomg inject web-1 --ssh-key ~/.ssh/id_ed25519
zomg inject <box> copies local credentials into the box.
FlagDefault
--codex-auth <PATH>~/.codex/auth.json
--codex-toml <PATH>~/.codex/config.toml
--ssh-key <PATH>First of ~/.ssh/id_ed25519, id_rsa, id_ecdsa
--project, -pTarget project
Destinations inside the box: /root/.codex/auth.json (chmod 600), /root/.codex/codex.toml (renamed from config.toml), /root/.ssh/<keyname> (chmod 600) plus .pub (644), and /root/.gitconfig. Git config is auto-discovered from GIT_CONFIG_GLOBAL, $XDG_CONFIG_HOME/git/config, ~/.config/git/config, or ~/.gitconfig. It seeds /root/.ssh/known_hosts with GitHub’s keys and tests ssh -T git@github.com. The final line reads:
OK: injected codex auth/config, ssh keypair[, git config] into <project>:<box>
inject has no --json.

JSON output

--json / -j is supported on create, list, show, update, publish, unpublish, delete, stop, resume, background exec, non-follow logs, port list, port wait, and the user list/create/update commands. It is not supported on foreground exec, ephemeral exec, logs --follow, console, ssh, cp/get/put, inject, or user delete.

CLI reference

Run zomg docs to print the bundled “Using zomg” guide. The full HTTP surface is documented in the API reference.

Next steps

Environment

The in-box runtime: PATH, /box/* volumes, env vars, and DNS.

Data volumes

Attach, share, and back up persistent data across boxes.

Services

Expose long-running services on a public URL with a port.

Snapshots & bases

Capture filesystem state and build tagged base images.