Inside a box, you get what looks like a fresh, privileged Linux machine: sessions run as your deployment user (root is an explicit --root away), PID 1 is tini, and there is no init system or Kubernetes identity to trip over. This page covers what the filesystem, environment, network, and lifecycle look like from a process running in the box. To drive the box from outside (creating, exec, console, ssh), see Boxes.

Mental model

  • A box is a single privileged container running a plain Ubuntu rootfs.
  • The runner bind-mounts kernel and system paths under /volumes/*, then chroot /volumes and starts tini as PID 1.
  • Sessions run as you — when you are signed in, exec, console, ssh, and dashboard sessions land as your deployment user. Root is explicit escalation (--root), and the anonymous deployment API token acts as root because it carries no user identity.
  • Sessions also land in the box’s PID, UTS, and IPC namespaces — ps, pgrep, and pkill in an exec, console, or ssh session see and signal box processes, and hostname is the box’s.
  • There is no systemd and no other init — tini reaps PID 1, nothing else.
  • The chrooted root filesystem is a copy-on-write btrfs subvolume. It survives stop/resume but is destroyed on delete.
PID 1  = /usr/bin/tini  (started via chroot /volumes)
user   = your deployment user (HOME=/home/<name>); root via --root or the deployment token
init   = none  (no systemd; tini only)

Detecting that you are inside a box

A startup script appends a source line for /etc/boxes-env to /etc/bash.bashrc, and the runner injects a set of environment variables. The most reliable inside-the-box check is whether BOX_NAME or ZOMG_PROJECT is set:
if [ -n "${BOX_NAME:-}" ]; then
  echo "Running inside box $BOX_NAME (project $ZOMG_PROJECT)"
fi
These variables are written both to the container environment and to /etc/boxes-env, so they are visible to any process — sh -c, cron jobs, Python subprocesses — not only to bash login shells.

Injected environment variables

VariableValue
ZOMG_PROJECTThe project name
BOX_NAMEThe box name
ZOMG_API_URLhttps://api.zomg.ai
ZOMG_API_HOSTapi.zomg.ai
ZOMG_API_IPThe node IP (Kubernetes status.hostIP)
ZOMG_NODE_IPThe node IP — same value as ZOMG_API_IP, second name
ZOMG_INSECURE"true", only when the API is configured insecure
TERMxterm-256color
ZOMG_API_IP and ZOMG_NODE_IP always resolve to the same value (the node IP), exposed under two names.

Reserved variables

These keys are reserved and cannot be set with -e at create time. Attempting to set one is rejected with environment variable is reserved:
ZOMG_PROJECT  BOX_NAME  ZOMG_API_URL  ZOMG_API_HOST  TERM
Your own -e KEY=VALUE pairs are written to /etc/boxes-env as export KEY="value" and added to the container environment, so they are visible to every process in the box.

Filesystem layout

The runner bind-mounts host paths under /volumes/* and chroots into /volumes. System volumes and persistent data are mounted at fixed paths; kernel and scratch surfaces are recreated on every pod start.

System volumes

PathBacking volumeScope
/box/binbox-binGlobal
/box/allbox-allGlobal
/box/projbox-projPer project
/homebox-homeGlobal
/box/* and /home are separate volumes from the box’s root filesystem, so they survive box delete. /home is one deployment-global volume shared by every box in every project: the same user gets the same uid and the same /home/<user> everywhere.

Kernel and scratch surfaces

Recreated on every pod start:
  • /proc, /sys, /dev — procfs, sysfs, and a recursive bind of the host /dev.
  • /tmp and /run — bind-mounted from an ephemeral host path.
  • /etc/resolv.conf and /etc/hosts — bind-mounted from the pod’s files.
/tmp and /run are ephemeral. They are wiped on every pod restart, including stop/resume. Do not store anything you need to keep there.

What survives what

StateStop / resumeDelete
Root filesystem (/, btrfs CoW)SurvivesDestroyed
/box/* system volumesSurvivesSurvives
/home (global volume)SurvivesSurvives
Normal data volumesSurvivesSurvives
Box-owned clone data volumesSurvivesDeleted after owning-box cleanup completes
/tmp, /runWipedWiped
For how the CoW root relates to bases and checkpoints, see Snapshots and bases. For attaching shared persistent disks, see Data volumes.

PATH and shell defaults

The base image sets the PATH in /etc/profile.d/boxes-path.sh and /etc/bash.bashrc:
export PATH="$HOME/.local/bin:/box/proj/bin:/box/bin:$PATH"
Session defaults:
  • HOME=/home/<name> for a user session, HOME=/root for a root session
  • TERM=xterm-256color
  • WORKDIR is /work, and /etc/bash.bashrc runs cd /work when an interactive shell sources it.

The bash-login gotcha

/box/bin and /box/proj/bin are added to the PATH only because the base image’s bash startup files run/etc/profile.d/boxes-path.sh for login shells and /etc/bash.bashrc for interactive shells. The bare system PATH does not include them. zomg exec runs commands through a login shell (bash -lc), so /etc/profile.d/boxes-path.sh runs and the /box directories appear on the PATH. But a non-bash child process — a sh -c spawned by Python, a cron job, anything that bypasses bash startup files — sees only the bare system PATH:
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
If such a process needs a binary from /box/bin or /box/proj/bin, reference it by absolute path or set the PATH explicitly.
The cd /work line lives in /etc/bash.bashrc, which bash sources for interactive shells. Interactive sessions (zomg console, zomg ssh) land in /work. A non-interactive zomg exec login shell does not necessarily start in /work — pass --wd/--cwd if you need a specific working directory.

eatmydata and fsync

The base sets LD_PRELOAD to libeatmydata.so in both /etc/profile.d/eatmydata.sh (an export for login shells) and /etc/environment (a bare assignment PAM applies to every session):
LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libeatmydata.so
eatmydata turns fsync() and related calls into no-ops, which speeds up package installs, database setup, and similar work in a disposable box. The tradeoff is that data is not flushed to disk as durably as usual.
If a specific command relies on real fsync durability, you can opt out for that command by clearing the preload for it:
LD_PRELOAD= mycommand

Preinstalled packages

The box-base image includes a standard toolbox:
bash build-essential ca-certificates curl wget ripgrep fd-find git less
vim nano openssh-client openssh-sftp-server jq procps psmisc htop lsof
iproute2 iputils-ping net-tools dnsutils netcat-openbsd ncurses-bin
util-linux tini python3 python3-pip python3-venv python-is-python3
tmux unzip zip eatmydata sudo
sudo is configured NOPASSWD:ALL, so a non-root deployment-user session can escalate to root without a password. Inside a box, the boundary is the box itself, not the user account.
The dev-base image layers on top of box-base and adds:
  • gnupg, gh (GitHub CLI)
  • Node.js 22 (from NodeSource)
  • @anthropic-ai/claude-code and @openai/codex (global npm)
  • bun and bunx
  • the VS Code CLI (code), for code serve-web and code tunnel — see Development
For building your own templates, see Snapshots and bases.

Networking and DNS

The pod’s DNS is configured for short, project-scoped names while keeping external lookups fast:
  • Search domains: <project>.boxes, boxes, <project>
  • ndots:1, so external FQDNs like archive.ubuntu.com resolve absolutely first.
The box’s Service is headless (clusterIP: None), so DNS returns the pod IP directly and all ports are reachable between boxes — there is no port allowlist for box-to-box traffic. To expose a box to the public web on a URL, see Services.

Resource limits

Kubernetes sets a memory request and a memory limit, plus a CPU request:
  • Memory is capped by the limit.
  • CPU has a request only — there is no hard CPU limit, so CPU is not capped by a quota.
free -m and nproc report the host’s values, not your box’s allocation — this is standard Linux behavior, not a Zomg feature. To see the actual memory cap, read the cgroup file:
cat /sys/fs/cgroup/memory.max
Because no CPU limit is set, cat /sys/fs/cgroup/cpu.max may report max (uncapped).

Users

Users are a single deployment-level registry — the lab-computer model. One user means one uid and one /home/<user> in every box in every project:
zomg user create alice
  • UIDs start at 20000 and are assigned sequentially across the deployment (max(existing) + 1), then persisted. UID equals GID.
  • Names must be 1–32 characters of [a-z0-9_-] and not a reserved system name.
  • Home is /home/<name> on the deployment-global home volume, default shell /bin/bash.
  • A user can be bound to an email. On first allowlisted Google sign-in, Zomg auto-provisions a user named after the email’s localpart (lowercased; characters outside [a-z0-9_-] become -). A pre-created email-less user with that name is claimed by the sign-in; a name already bound to a different email is a conflict the operator resolves by updating user emails.
  • zomg user delete keeps /home/<name> by default; pass --purge-home to remove it.
When you are signed in, exec, console, ssh, and dashboard sessions run as your user with no flag needed. Pass --root for an explicit root session, or --user <name> for another user. The anonymous deployment API token has no user identity and acts as root. When a session runs as a user, the runner writes that user’s /etc/passwd, /etc/group, and /etc/shadow entries on the fly: it removes any prior line for that same user, appends a fresh one, and creates /home/<name> if needed. (The /etc/shadow entry is what lets sudo’s PAM account phase succeed.) Entries for other users accumulate across execs and are not removed.

Lifecycle from inside

1

Stop

The deployment scales to zero. Every process dies, and /tmp and /run are lost. The root filesystem is preserved.
2

Resume

A new pod starts with a fresh tini as PID 1. Environment variables are re-injected and system mounts are re-applied. Your processes do not survive — only the filesystem does.
3

Delete

The root subvolume is destroyed. System volumes (/box/*, /home) and normal persistent data volumes survive. Box-owned clone data volumes are the exception: Zomg deletes them with the owning box after cleanup completes.
Changing environment variables on an existing box does not work. zomg update only applies CPU, memory, termination grace period, and tags — arbitrary KEY=VALUE env changes are silently ignored. To change real environment variables, recreate the box with -e.

Next steps

Boxes

Create, exec, console, and ssh into boxes from outside.

Data volumes

Attach shared persistent disks across boxes.

Services

Expose a box to a public URL.

API reference

Manage boxes programmatically over HTTP.