# Configuration reference

> Compose-backed env keys: preview domain/ports, SANDBOXED_DATA_DIR, API bind, auth tokens, idle/reaper/memory wake tuning, templates/library paths, and advanced cgroup toggles.

- Repository: tastyeffectco/sandboxes
- GitHub: https://github.com/tastyeffectco/sandboxes
- Human docs: https://grok-wiki.com/public/docs/tastyeffectco-sandboxes-f551c1a2e9a0
- Complete Markdown: https://grok-wiki.com/public/docs/tastyeffectco-sandboxes-f551c1a2e9a0/llms-full.txt

## Source Files

- `.env.example`
- `docker-compose.yml`
- `control-plane/cmd/sandboxd/main.go`
- `control-plane/internal/auth/config.go`
- `README.md`
- `control-plane/README.md`

---

---
title: "Configuration reference"
description: "Compose-backed env keys: preview domain/ports, SANDBOXED_DATA_DIR, API bind, auth tokens, idle/reaper/memory wake tuning, templates/library paths, and advanced cgroup toggles."
---

Runtime configuration for the sandboxed stack is a single host-level `.env` file (bootstrapped from `.env.example` by `install.sh`) that `docker compose` substitutes into `docker-compose.yml` and passes into the `sandboxd` container as process environment variables. Traefik reads compose-level keys (`HTTP_PORT`, log mounts); `sandboxd` reads the rest at startup and on `SIGHUP` for auth-only reload.

```text
  .env  ──substitute──►  docker-compose.yml
                              │
              ┌───────────────┴───────────────┐
              ▼                               ▼
          traefik                         sandboxd
     (ports, log mount)              (all SANDBOXD_* / SANDBOXED_* env)
```

<Note>
Changing `.env` requires `docker compose up -d` (or `restart sandboxd`) for most keys. Auth token rotation can use `SIGHUP` on `sandboxd` when `SANDBOXD_ENV_FILE` is configured — see [API authentication](/api-authentication).
</Note>

## Compose-backed keys (`.env.example`)

These keys are documented in `.env.example`, wired in `docker-compose.yml`, and are the knobs most operators touch.

### Preview routing

| Variable | Default | Scope | Effect |
|---|---|---|---|
| `PREVIEW_DOMAIN` | `localhost` | Traefik + sandboxd | Host suffix for preview URLs: `s-{id}-{port}.preview.{domain}` |
| `PREVIEW_ENTRYPOINT` | `web` | sandboxd | Traefik entrypoint name on per-sandbox Docker labels |
| `PREVIEW_TLS` | `false` | sandboxd | When `true`, emits `traefik.http.routers.*.tls=true` on sandbox labels |
| `HTTP_PORT` | `80` | Traefik (compose) | Host port mapped to Traefik `:80`; append `:{port}` to preview URLs when not 80 |

Browsers resolve `*.localhost` to `127.0.0.1`, so the default stack needs no DNS or certificates. For a public wildcard domain, set `PREVIEW_DOMAIN` to your apex, enable Traefik `websecure` + a cert resolver in `traefik/traefik.yml`, uncomment the `HTTPS_PORT` mapping in compose, and set `PREVIEW_ENTRYPOINT=websecure` and `PREVIEW_TLS=true`.

### Images and Docker network

| Variable | Default | Effect |
|---|---|---|
| `SANDBOXED_IMAGE` | `sandboxed-base:1.0.0` | Base image for per-sandbox containers (compose maps to `SANDBOXD_IMAGE` inside sandboxd) |
| `SANDBOXED_NETWORK` | `sandboxed_net` | Docker network name; must match Traefik's docker provider `network` in `traefik/traefik.yml` |

### Storage and API publish

| Variable | Default | Effect |
|---|---|---|
| `SANDBOXED_DATA_DIR` | `/var/lib/sandboxed` | **Absolute** host path bind-mounted symmetrically into sandboxd; holds workspaces, SQLite, templates, library |
| `SANDBOXED_LOG_DIR` | `/var/lib/sandboxed/log` | Shared log directory (Traefik access log + tailer checkpoint) |
| `SANDBOXED_API_BIND` | `127.0.0.1:9090` | Host `host:port` published to sandboxd internal `:9000` |

Derived layout under `SANDBOXED_DATA_DIR` (created by sandboxd):

```text
  {SANDBOXED_DATA_DIR}/
    workspaces/<ulid>/     # per-sandbox bind mount → /home/sandbox
    state/sandboxd.db      # SQLite (override path with SANDBOXD_DB)
    templates/             # golden templates (unless SANDBOXD_TEMPLATES_DIR set)
    library/               # snapshot spin-up images (unless SANDBOXD_LIBRARY_DIR set)
    _snapshots/            # manual snapshot storage root
```

<Warning>
`SANDBOXED_DATA_DIR` must stay an absolute path with the same value on host and inside the sandboxd container. The OSS build stores workspaces as plain directories with **no** hard per-workspace disk quota.
</Warning>

### API authentication (compose defaults)

| Variable | Default | Effect |
|---|---|---|
| `SANDBOXD_API_AUTH_DISABLED` | `true` | When `true`/`1`/`yes`/`on`, external API calls skip bearer checks. Set `false` and supply tokens for production |
| `SANDBOXD_API_TOKENS` | *(empty)* | Comma-separated `name:secret` pairs; clients send `Authorization: Bearer <secret>` |

With auth enabled (`SANDBOXD_API_AUTH_DISABLED=false`) and an empty token list, every non-loopback API call returns 401 while loopback still works.

### Resource policy (compose)

| Variable | Default | Effect |
|---|---|---|
| `SANDBOXD_IDLE_THRESHOLD_SECONDS` | `2100` | Seconds without activity before idle reaper runs `docker stop` (workspace preserved) |
| `SANDBOXED_SET_MEMORY_HIGH` | `false` | When `true`, sandboxd writes cgroup v2 `memory.high` after start/wake/reconcile (needs host cgroup visibility) |

Per-sandbox hard limits are fixed in create logic (`--memory=10g`, `--memory-swap=10g`, `memory_high` request default `4G` when unset) — not `.env` tunables.

## How compose maps into sandboxd

`docker-compose.yml` passes a subset of `.env` into the `sandboxd` service `environment` block. Host-only keys (`HTTP_PORT`, `SANDBOXED_API_BIND`) affect compose port mappings only.

| `.env` key | Container env | Notes |
|---|---|---|
| `SANDBOXED_IMAGE` | `SANDBOXD_IMAGE` | Name differs: compose uses `SANDBOXED_*`, process reads `SANDBOXD_IMAGE` |
| `SANDBOXED_LOG_DIR` | `SANDBOXD_ACCESS_LOG` | Set to `{SANDBOXED_LOG_DIR}/traefik-access.log` |
| `SANDBOXED_USERNS` | `SANDBOXED_USERNS` | Not in `.env.example`; compose default `host` — see [Workspaces and isolation](/workspaces-persistence) |

Infrastructure containers (`traefik`, `sandboxd`) use `userns_mode: host` in compose so they work on daemons with or without `userns-remap`. Sandboxes inherit `SANDBOXED_USERNS` (default `host`) on `docker run`.

## Auth and private previews (sandboxd env)

Not all auth keys appear in `.env.example`; add them to the `sandboxd` `environment` section (or a mounted env file) when needed.

<ParamField body="SANDBOXD_PREVIEW_TOKEN_SECRETS" type="string">
Comma-separated `kid:secret` map for preview JWT signing (private sandboxes).
</ParamField>

<ParamField body="SANDBOXD_AUTH_REDIRECT_URL" type="string">
Template URL for `/preview-auth` redirects; supports `{sandbox_id}` and `{return}` placeholders.
</ParamField>

<ParamField body="SANDBOXD_FORWARD_AUTH_DENY_MODE" type="string" default="redirect">
How `/forward-auth` denies unauthenticated viewers (`redirect` or `meta-refresh`).
</ParamField>

<ParamField body="SANDBOXD_ENV_FILE" type="string" default="/etc/sandboxed/sandboxd.env">
Systemd-style `KEY=value` file re-read on `SIGHUP` for token rotation without restart.
</ParamField>

Private sandboxes attach Traefik `sandbox-preview-auth@file` (see `traefik/dynamic/auth.yml`), which calls `http://sandboxd:9000/forward-auth`.

## Idle, wake, and memory pressure

These variables are read in `cmd/sandboxd/main.go` at startup. Only `SANDBOXD_IDLE_THRESHOLD_SECONDS` is in the stock compose file; add others under `sandboxd.environment` to tune behavior.

### Idle reaper

| Variable | Default | Effect |
|---|---|---|
| `SANDBOXD_IDLE_THRESHOLD_SECONDS` | `2100` | No activity → candidate for `docker stop` |
| `SANDBOXD_IDLE_REAP_INTERVAL_SECONDS` | `30` | Tick interval for idle scan |
| `SANDBOXD_WAKE_GRACE_SECONDS` | `60` | After last access-log activity, grace before idle stop (widens when connection poller is in fallback mode) |

Activity is driven primarily by the Traefik access-log tailer (`SANDBOXD_ACCESS_LOG`, default `{SANDBOXED_LOG_DIR}/traefik-access.log`) and optionally an open-connection poller.

### Host memory pressure reaper

| Variable | Default | Effect |
|---|---|---|
| `SANDBOXD_PRESSURE_INTERVAL_SECONDS` | `10` | Tick interval; `<= 0` disables the loop |
| `SANDBOXD_MEM_HEADROOM_PCT` | `15` | Healthy floor — startup runs one synchronous tick if below |
| `SANDBOXD_MEM_REFUSE_WAKES_PCT` | `10` | Wake admission refuses new starts below this MemAvailable % |
| `SANDBOXD_MEM_EMERGENCY_PCT` | `5` | Emergency band — stops sandboxes by RSS / idle ranking |

### Wake admission and keepalive

| Variable | Default | Effect |
|---|---|---|
| `SANDBOXD_WAKE_COST_MB` | `800` | Estimated RAM cost per wake for admission math |
| `SANDBOXD_WAKE_TCP_READY_TIMEOUT_SECONDS` | `8` | Wait for preview port TCP after `docker start` |
| `SANDBOXD_KEEPALIVE_MAX_SECONDS` | `86400` | Upper bound for `POST .../keepalive` extensions |

Stopped sandboxes wake on preview hits via the Traefik file-provider catch-all (`traefik/dynamic/wake.yml`, priority 1) → `http://sandboxd:9000`.

### Activity poller (optional)

| Variable | Default | Effect |
|---|---|---|
| `SANDBOXD_POLLER_METRIC_RE` | *(unset)* | Regex matching a Traefik open-connection metric name; unset → fallback mode (access log only) |
| `SANDBOXD_POLLER_URL` | `http://127.0.0.1:8082/metrics` | Metrics scrape URL |
| `SANDBOXD_POLLER_INTERVAL_SECONDS` | `15` | Scrape interval |
| `SANDBOXD_POLLER_SERVICE_LABEL` | `service` | Label name on matched metric series |

## Templates and library paths

| Variable | Default | Effect |
|---|---|---|
| `SANDBOXD_TEMPLATES_DIR` | `{SANDBOXED_DATA_DIR}/templates` | Directory of golden template workspaces (`template` field on create) |
| `SANDBOXD_LIBRARY_DIR` | `{SANDBOXED_DATA_DIR}/library` | Allowed root for `template_path` snapshot spin-up |

Create accepts `template` (name under `SANDBOXD_TEMPLATES_DIR`) or `template_path` (must resolve under library or templates roots). If `SANDBOXD_TEMPLATES_DIR` is unset/empty, named templates return 503.

Snapshot-related defaults (manual API; auto-snapshotter disabled in OSS):

| Variable | Default |
|---|---|
| `SANDBOXD_SNAPSHOT_RETENTION_DAYS` | `7` |
| `SANDBOXD_SNAPSHOT_IDLE_HOURS` | `24` |

## Advanced cgroup and container toggles

<ParamField body="SANDBOXED_SET_MEMORY_HIGH" type="boolean" default="false">
After `docker run` / wake / reconcile, write `memory.high` to `/sys/fs/cgroup/.../memory.high` using the sandbox row's value (create default `4G`). Failures are logged but do not fail create — hard `--memory=10g` still applies.
</ParamField>

<ParamField body="SANDBOXED_USERNS" type="string" default="host">
Passed as `docker run --userns` for sandboxes and the workspace seed container. Set empty to use the daemon default (see ARCHITECTURE.md userns-remap note).
</ParamField>

Create-time API field `memory_high` (JSON) sets the per-sandbox soft throttle stored in SQLite and re-applied on wake when `SANDBOXED_SET_MEMORY_HIGH=true`.

## Other sandboxd environment variables

| Variable | Default | Purpose |
|---|---|---|
| `SANDBOXD_ADDR` | `0.0.0.0:9000` | Listen address inside the container (compose publishes via `SANDBOXED_API_BIND`) |
| `SANDBOXD_DB` | `{dataDir}/state/sandboxd.db` | SQLite DSN path |
| `SANDBOXD_MIGRATIONS` | `/usr/local/share/sandboxd/migrations` | SQL migration directory |
| `SANDBOXD_TAILER_OFFSET` | `{dataDir}/state/traefik-tail.offset` | Access-log tailer checkpoint |
| `SANDBOXD_DEBUG` | *(unset)* | Any non-empty value sets log level to debug |
| `SANDBOXD_LLM_TXT_PATH` | `/etc/sandboxed/llm.txt` | Path for `GET /llm.txt` integrator contract |
| `SANDBOXD_GIT_TOKEN_PATH` | `/etc/sandboxed/git/token` | Host credential file for HTTPS git clone |

Subsystems **disabled** in the OSS compose build (code retained; enabling requires extra host services):

| Variable | Purpose |
|---|---|
| `SANDBOXED_NGINX_WATCH_PATHS` / `SANDBOXED_NGINX_CONTAINER` | Hot-reload a host nginx registry proxy |
| Egress manager | nftables/journald — always `nil` in portable build |

## Traefik static paths (not `.env`)

`traefik/traefik.yml` hardcodes the docker provider constraint `Label(\`sandboxed.managed\`,\`true\`)` and default access log path `/var/lib/sandboxed/log/traefik-access.log`. Keep `SANDBOXED_LOG_DIR` aligned with that mount, or edit the static config to match your log dir.

File-provider routes in `traefik/dynamic/`:

| File | Role |
|---|---|
| `wake.yml` | Catch-all `HostRegexp` for stopped-sandbox wake (priority 1) |
| `api.yml` | Optional `api.preview.*` → sandboxd (loopback bind remains primary) |
| `auth.yml` | Forward-auth middleware for `visibility=private` |

## Example `.env` profiles

<Tabs>
  <Tab title="Local (default)">
```bash
PREVIEW_DOMAIN=localhost
HTTP_PORT=80
SANDBOXED_DATA_DIR=/var/lib/sandboxed
SANDBOXED_API_BIND=127.0.0.1:9090
SANDBOXD_API_AUTH_DISABLED=true
SANDBOXD_IDLE_THRESHOLD_SECONDS=2100
```
  </Tab>
  <Tab title="Port 80 busy">
```bash
HTTP_PORT=8088
# Preview: http://s-<id>-<port>.preview.localhost:8088
```
  </Tab>
  <Tab title="LAN API + auth">
```bash
SANDBOXED_API_BIND=0.0.0.0:9090
SANDBOXD_API_AUTH_DISABLED=false
SANDBOXD_API_TOKENS=integrator:your-secret-here
```
  </Tab>
  <Tab title="Production TLS">
```bash
PREVIEW_DOMAIN=yourdomain.com
PREVIEW_ENTRYPOINT=websecure
PREVIEW_TLS=true
SANDBOXD_API_AUTH_DISABLED=false
SANDBOXD_API_TOKENS=prod:secret
# Also: traefik websecure entrypoint + cert resolver, HTTPS_PORT in compose
```
  </Tab>
</Tabs>

## Apply and verify

<Steps>
  <Step title="Edit configuration">
Copy `.env.example` to `.env` if missing, then set keys. For sandboxd-only variables, extend the `sandboxd.environment` list in `docker-compose.yml`.
  </Step>
  <Step title="Recreate the stack">
```bash
docker compose up -d
```
`install.sh` is idempotent and loads `.env` before creating `SANDBOXED_DATA_DIR` and building images.
  </Step>
  <Step title="Check health and binding">
```bash
curl -s http://127.0.0.1:9090/healthz   # ok
curl -s http://127.0.0.1:9090/readyz    # ready
```
Use your actual `SANDBOXED_API_BIND` host:port if changed.
  </Step>
  <Step title="Confirm preview URL shape">
After creating a sandbox on port 3000, open  
`http://s-{id}-3000.preview.{PREVIEW_DOMAIN}`  
and append `:{HTTP_PORT}` when `HTTP_PORT` ≠ 80.
  </Step>
</Steps>

<Check>
After changing auth tokens in a mounted env file, send `SIGHUP` to the sandboxd process (or `docker compose restart sandboxd`) and watch logs for `reload: auth config reloaded`.
</Check>

## Related pages

<CardGroup>
  <Card title="Installation" href="/installation">
Bootstrap `.env`, build images, and bring up compose with health checks.
  </Card>
  <Card title="Preview routing" href="/preview-routing">
Traefik labels, wake catch-all priority, and `PREVIEW_DOMAIN` host rules.
  </Card>
  <Card title="Wake, idle, and pressure" href="/wake-idle-reapers">
How idle threshold, pressure bands, and wake admission interact at runtime.
  </Card>
  <Card title="API authentication" href="/api-authentication">
Bearer tokens, loopback exemptions, `SIGHUP` reload, and LAN exposure risks.
  </Card>
  <Card title="Production deployment" href="/production-deployment">
Wildcard DNS, `websecure`, TLS store, and hardening checklist.
  </Card>
  <Card title="Preview URL reference" href="/preview-url-reference">
Exact hostname pattern and `HTTP_PORT` suffix rules.
  </Card>
</CardGroup>
