A box runs your processes inside a chrooted Ubuntu rootfs. From inside, it looks like a fresh, privileged Linux machine: you are root, PID 1 is tini, and there is no init system or Kubernetes identity to trip over. This page describes that runtime — what the filesystem, environment, network, and lifecycle look like from a process executing 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.
  • You run as root by default. 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   = root  (HOME=/root)
init   = none  (no systemd; tini only)
Internally the API calls a box a “fork” (you will see fork-prefixed labels and a container literally named fork). User-facing tools and this documentation call it a box. Both terms refer to the same thing.

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 the presence of BOX_NAME or FORKR_PROJECT:
if [ -n "${BOX_NAME:-}" ]; then
  echo "Running inside box $BOX_NAME (project $FORKR_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
FORKR_PROJECTThe project name
BOX_NAMEThe box name
FORKR_API_URLhttps://forks.<domain>
FORKR_API_HOSTforks.<domain>
FORKR_API_IPThe node IP (Kubernetes status.hostIP)
FORKR_NODE_IPThe node IP — same value as FORKR_API_IP, second name
FORKR_INSECURE"true", only when the API is configured insecure
TERMxterm-256color
FORKR_API_IP and FORKR_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:
FORKR_PROJECT  BOX_NAME  FORKR_API_URL  FORKR_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
/homefork-homePer project
/box/* and /home are separate volumes from the box’s root filesystem, so they survive box delete.

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 (project volume)SurvivesSurvives
Data volumesSurvivesSurvives
/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/forks-path.sh and /etc/bash.bashrc:
export PATH="$HOME/.local/bin:/box/proj/bin:/box/bin:$PATH"
Defaults for the root user:
  • HOME=/root
  • TERM=xterm-256color
  • WORKDIR is /work, and /root/.bashrc runs cd /work when it is sourced.

The bash-login gotcha

/box/bin and /box/proj/bin are added to the PATH only because /etc/profile runs. The base PATH does not include them. 4kr exec runs commands through a login shell (bash -lc), so /etc/profile.d/forks-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 /root/.bashrc, which bash sources for interactive shells. Interactive sessions (4kr console, 4kr ssh) land in /work. A non-interactive 4kr 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 (login shells) and /etc/environment:
export LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libeatmydata.so
eatmydata turns fsync() and related calls into no-ops, which makes operations like package installs and database setup much faster inside a disposable box. The tradeoff is that data is not flushed to disk as durably as it would be normally.
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
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
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>.forks, forks, <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 Forkr 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).

Project users

By default everything runs as root. You can create project-scoped users:
4kr user create alice
  • UIDs start at 20000 and are assigned sequentially per project (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>, default shell /bin/bash.
When you exec as a user, the runner synthesizes that user’s /etc/passwd and /etc/group entries on the fly: it removes any prior line for that same user and appends a fresh one. 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 data volumes survive.
Changing environment variables on an existing box does not work. 4kr update only applies PORT, 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.