BettikDocsSign in

Reference · the environment directory

The environment directory

An environment is a directory that Docker runs. Everything Docker needs and everything you change lives in ~/environments/<name>/, and a template is a copy of one. This page is the contract: every file, when Bettik runs it, and what it finds in its environment variables.

The files

bettik.json
The contract. Which service is the app and on what port; which secrets to mint; what .env should say; how many seconds each script gets. Read by Bettik; never run.
compose.yaml
The setup: the app's runtime and the services beside it. Docker Compose and only Docker Compose — yours to edit. Every ${VARIABLE} in it is supplied by Bettik. Applied by build_environment.
seed
Runs once, when the environment is installed, inside the app container. Make the directory yours: clone or create the project, create schema, load fixtures, write config. From a saved environment it also adopts: it gets OLD_* beside the fresh secrets and rotates what it knows how to. Exit non-zero and the install fails, with the last lines as the reason.
update
Bring a saved environment current: pull, reinstall, migrate. Runs after seed when the environment was installed from a saved environment, and whenever update_environment is called. Must be safe to run twice.
start
Runs on every start, in the app container, and stays in the foreground: start the processes your work needs. The container runs it again whenever it exits, so an environment with nothing to run yet idles rather than crash-looping, and starts the moment start has something to start. It runs in a process group of its own, so a trap "kill 0" EXIT in it stops what it started and not the container (compose.yaml is the environment's own and Bettik never rewrites it, so an older environment keeps the loop it was made with: copy the starter kit's command to have this). No timeout.
stop
Optional. A graceful stop, with thirty seconds. Without it the containers are simply stopped, which is fine for a dev server.
.env
What Bettik knows, written on every start: the environment's name and address, every port, every secret, and the env map from bettik.json expanded. Edits do not survive. The only place the credentials are.
projects/
The code. One or more repositories, created or cloned in. An environment may put things here when they are part of its setup (a WordPress checkout) and does not when they are not.
CLAUDE.md
The guide a session reads when it enters the environment. Seeded once, yours after that.
README-bettik.md
What the environment says about itself.
.bettik/
Bettik's own notes: where the environment came from (origin.json), and for an environment installed from a template the secrets its data was made with, mode 0600, removed once read.

The scripts are free-form executables. Each ships as a commented skeleton stating its own contract and a worked example for that environment.

The one rule

Whatever you do by hand that a fresh copy would also need, put in seed or update. Cloned a repository? The git clone line goes in seed. Created a schema? The migration goes in seed, and its re-run in update. Changed the setup? That is already in compose.yaml. This is what makes save_environment and publish_template produce a template version that works when installed, and it costs one line at the time you did the thing.

bettik.json

{
  "version": 3,
  "description": "Node 22 for the app and Postgres 17 beside it.",
  "app": { "service": "web", "port": 3000 },
  "urls": {},
  "ports": [],
  "secrets": ["DB_PASSWORD"],
  "env": {
    "DATABASE_URL": "postgresql://app:${DB_PASSWORD}@${_BETTIK_ENVIRONMENT}-db:5432/app"
  },
  "summary": {
    "runtime": "Node 22 (node:22-trixie-slim), listening on $PORT",
    "services": ["Postgres 17 at <environment>-db:5432, database app, role app"],
    "projects": "empty -- create a project or clone one in, then start it from start"
  },
  "timeouts": { "seed": 1800, "update": 1800, "stop": 30 }
}
app
Which compose service the environment's hostname routes to, and on what port inside the container. The scripts run in this container too.
requires
What the environment needs from whoever installs it: env vars from the settings page, by name, each with a description, and optionally a default or "optional": true. Handed to seed, update and stop and nothing else of theirs; "env": true also writes it to .env. An environment needing one that is not set refuses to install, and says which.
open
Beside app or a urls entry: paths on a private environment that anyone may reach -- ["/webhook", "/hooks/*"]. See leaving a path open.
urls
Every other hostname the environment answers on. "api" is api-<name>.home.<handle>.bettik.dev, routed to a service and port. Each gets a host port of its own.
widget
A small view of the environment drawn on its card -- { "url": "monitor", "path": "/widget.html", "height": 64 }. See a widget on its card.
ports
Host ports with no hostname — a debugger, something to tunnel to. Names only; Bettik allocates the numbers.
grants
What the environment may ask its box to do to itself, with no Claude in the way: snapshot (save itself), prune (delete its own older saved environments) and gateway (post to these topics, "topics": ["spaces"]). Each with a description that the template page shows before anyone installs it. See grants.
secrets
Names. Bettik mints a random value for each, per environment, and keeps it for the environment's life. A name added later is minted on the next build.
env
What .env should say beyond the BETTIK_* variables and the secrets, with ${VAR} expanded from both. The app container gets these too.
summary
What a person is getting, said plainly. This is what list_templates and install_environment report.
timeouts
Seconds before Bettik gives up on a script. start has none.

The variables

Supplied to compose.yaml, written to .env, and in the process environment of every script. Never write a port, a path or a password into compose.yaml yourself: they are allocated per environment and would collide with the next one.

The leading underscore marks a name as Bettik's, so it can never clash with one of yours. The names from before it (BETTIK_STACK, BETTIK_PORT and so on) are no longer set, so an environment that still uses one gets an empty value. build_environment and start_environment say which of compose.yaml, bettik.json and the scripts still use one and what it is called now, and save_environment refuses an environment that does.

_BETTIK_ENVIRONMENT
The environment's name: catan.
_BETTIK_OWNER
The box: <box>. Pass it as box from a session on the box.
_BETTIK_APP_HOST
The environment's hostname: catan.home.<handle>.bettik.dev. Frameworks treat a proxied hostname as a cross-origin attack unless told: Next allowedDevOrigins, Vite server.allowedHosts, Django ALLOWED_HOSTS, Rails config.hosts.
_BETTIK_APP_URL
The same, as https://….
_BETTIK_PORT
The host port behind the environment's hostname. In compose.yaml: "127.0.0.1:${_BETTIK_PORT}:3000". Inside the container the app listens on its own port — $PORT, 3000 — and Bettik never changes that.
_BETTIK_PORT_<NAME>
One per entry in urls and ports, uppercased, hyphens to underscores: _BETTIK_PORT_API. Allocated at first build, kept for the environment's life, freed when the name goes.
_BETTIK_HOST_<NAME>, _BETTIK_URL_<NAME>
The hostname and URL of each urls entry.
_BETTIK_PROJECT
The compose project name: for container_name, so two environments' containers never collide.
_BETTIK_DATA
The environment's directory on the host, for bind mounts: ${_BETTIK_DATA}:/bundle.
_BETTIK_NETWORK
This environment's own network. Its services and the workbench are on it, and no other environment is.
_BETTIK_GRANT_TOKEN, _BETTIK_GRANT_URL
Only for an environment with grants: its token, and where to send it. See grants.
the secrets
Each name in secrets, with its minted value.
the env map
DATABASE_URL and whatever else bettik.json says, expanded.

In the scripts, additionally:

_BETTIK_SEED_FROM
blank, or snapshot for a saved environment: how this environment was installed. seed only.
OLD_*
When from a saved environment: every secret and env value as the data was made with. seed only, for rotating.
GITHUB_TOKEN
The owner's token, if they set one in Settings. For cloning; never write it into a file.

Grants: what an environment may ask its box

An environment can ask its box to do a few things to itself, from its own code, on its own clock: back itself up every night, keep only the newest backups, tell your sessions something happened. It names them in bettik.json, each with a line saying what it is for:

"grants": {
  "snapshot": { "description": "a backup of your notes every night" },
  "prune":    { "description": "keep only the last two backups" },
  "gateway":  { "description": "tell your sessions when someone shares a note", "topics": ["spaces"] }
}

The template page shows them under What it may do before anyone installs it, like an app's permissions. When the environment starts, the box writes one token for exactly these into .env, _BETTIK_GRANT_TOKEN, and where to send it, _BETTIK_GRANT_URL. Pass them into the app in compose.yaml like any other variable. Every call carries Authorization: Bearer $_BETTIK_GRANT_TOKEN:

POST /bundle/snapshot
Save this environment, as save_environment does: kept in the account that owns the box, under the environment's own name. It answers at once with 202; the environment is paused for the few seconds the copy takes. snapshot.
GET /bundle/snapshots
Its own saved environments, newest first, and how the last one it asked for went. snapshot.
POST /bundle/prune
Delete its own saved environments but the newest { "keep": 2 } (1 to 20, two if left out). Only ones its grant took: never a saved environment you saved yourself, never one still being stored. prune.
POST /topics/<topic>
Post to a topic it was granted, or one under it, as the gateway takes from anyone. The message says it is from bundle-<name>. gateway.
GET /bundle
Which environment the token is, and what it was granted.

What the environment may do is what you agreed to: the grants its template showed when you installed it, or what bettik.json says when you (or Claude, for you) run build_environment. A start never adds one, so an environment cannot grant itself more by editing its own bettik.json; a grant it stops asking for is dropped at once. A clone has what its source had.

The token acts on that one environment and nothing else: no path or body names another, it cannot read topics, and it never reaches the box, the account or your other environments. A clone gets a token of its own, a saved environment carries none, and destroying the environment ends it. Scheduling is the environment's own: a process inside it runs on its own clock. An environment is refused, and told to rename, when its saved environment would land in a template someone else made.

compose.yaml, the parts Bettik relies on

Two things in the shipped file are load-bearing and worth keeping when you rewrite it:

A shell $ in the file must be written $$: Compose interpolates the file first. Tools your scripts need that the runtime image lacks — git on a slim image, pnpm, psql — belong in an image/Dockerfile beside compose.yaml (build: ./image), not installed at seed time into a container that will be replaced.

Memory, CPU and process limits are applied by Bettik to every container after the environment comes up, not read from the file: a mem_limit in compose.yaml is replaced by the box's own ceiling per container.

The lifecycle, as lived

Install empty. Edit compose.yaml until the setup is the one you need and build_environment. Put a project in projects/ and a line in start; write the step into seed. Save it. Next time, install that. Clone before an upgrade; edit the clone's compose.yaml; update_environment; keep it or throw it away.