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/*, thenchroot /volumesand startstinias PID 1. - You run as root by default. There is no systemd and no other init —
tinireaps PID 1, nothing else. - The chrooted root filesystem is a copy-on-write btrfs subvolume. It survives stop/resume but is destroyed on delete.
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:
/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
| Variable | Value |
|---|---|
FORKR_PROJECT | The project name |
BOX_NAME | The box name |
FORKR_API_URL | https://forks.<domain> |
FORKR_API_HOST | forks.<domain> |
FORKR_API_IP | The node IP (Kubernetes status.hostIP) |
FORKR_NODE_IP | The node IP — same value as FORKR_API_IP, second name |
FORKR_INSECURE | "true", only when the API is configured insecure |
TERM | xterm-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:
-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
| Path | Backing volume | Scope |
|---|---|---|
/box/bin | box-bin | Global |
/box/all | box-all | Global |
/box/proj | box-proj | Per project |
/home | fork-home | Per 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./tmpand/run— bind-mounted from an ephemeral host path./etc/resolv.confand/etc/hosts— bind-mounted from the pod’s files.
What survives what
| State | Stop / resume | Delete |
|---|---|---|
Root filesystem (/, btrfs CoW) | Survives | Destroyed |
/box/* system volumes | Survives | Survives |
/home (project volume) | Survives | Survives |
| Data volumes | Survives | Survives |
/tmp, /run | Wiped | Wiped |
PATH and shell defaults
The base image sets the PATH in/etc/profile.d/forks-path.sh and /etc/bash.bashrc:
HOME=/rootTERM=xterm-256colorWORKDIRis/work, and/root/.bashrcrunscd /workwhen 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:
/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 setsLD_PRELOAD to libeatmydata.so in both /etc/profile.d/eatmydata.sh (login shells) and /etc/environment:
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.
Preinstalled packages
Thebox-base image includes a standard toolbox:
dev-base image layers on top of box-base and adds:
gnupg,gh(GitHub CLI)- Node.js 22 (from NodeSource)
@anthropic-ai/claude-codeand@openai/codex(global npm)bunandbunx- the VS Code CLI (
code), forcode serve-webandcode tunnel— see Development
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 likearchive.ubuntu.comresolve absolutely first.
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.
Project users
By default everything runs as root. You can create project-scoped users:- UIDs start at
20000and 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.
/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
Stop
The deployment scales to zero. Every process dies, and
/tmp and /run are lost. The root filesystem is preserved.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.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.