Create, run, and manage boxes — the core sandbox lifecycle object in Zomg.
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.
The CLI needs a base URL and a token to reach the API. The URL resolves in this priority:
The --url flag.
The selected profile (the --profile flag or ZOMG_PROFILE, plus ZOMG_PROFILE_<NAME>_URL).
ZOMG_URL.
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=stagingexport ZOMG_PROFILE_STAGING_URL=https://api.zomg.example.comexport 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.
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:
--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 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.
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.
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.
Flag
Purpose
--force, -f
Delete even if the box is running.
--wait
Poll 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, -j
Project / 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:
Status
Meaning
deleting
Cleanup is still in progress or retrying.
deleted
Cleanup finished, or no box by that name exists.
failed
Background retry exhausted and manual repair may be needed.
active
The box exists and is not currently being deleted.
zomg publish <box> [subdomain] exposes the box on a custom hostname with TLS. unpublish removes it.
Flag
Purpose
subdomain
Required 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, -f
Take over a subdomain already published elsewhere.
--project, -p / --json, -j
Project / 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.
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 request
Resolves to
<box>
A box in the same project P.
<box>.<other-project>
A box in another project.
<box>.default
A box in the default project.
any dotted FQDN
External DNS.
# from inside a box, reach a sibling box named "db" on port 5432psql -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.
zomg exec <box> [flags] -- <cmd...> runs a command in a box.
Flag
Purpose
--user <NAME>
Run as a project user.
--env KEY=VALUE
Set an environment variable (repeatable).
--wd <DIR> (alias --cwd)
Working directory.
--sh (alias --bash)
Treat the args as a shell string.
--bg
Run detached in the background; returns a PID.
--project, -p / --json, -j
Project / 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.
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:
zomg cp ./app.tar web-1:/srv/app.tarzomg get web-1:/var/log/app.log ./app.logzomg 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).
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.
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 --versionzomg 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.
zomg inject <box> copies local credentials into the box.
Flag
Default
--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, -p
Target 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>
--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.