# Installation

> Prerequisites (Docker Engine + Compose on Linux), ./install.sh steps, .env bootstrap, base-image and control-plane build, compose up, and healthz/readyz verification.

- 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

- `install.sh`
- `.env.example`
- `docker-compose.yml`
- `image/build.sh`
- `image/Dockerfile`
- `control-plane/Dockerfile`

---

---
title: "Installation"
description: "Prerequisites (Docker Engine + Compose on Linux), ./install.sh steps, .env bootstrap, base-image and control-plane build, compose up, and healthz/readyz verification."
---

`./install.sh` brings up the full sandboxed stack on a single Linux host: it validates Docker and Compose, bootstraps `.env`, builds the `sandboxed-base` image and the `sandboxd` control-plane image, creates the data directory, and runs `docker compose up -d` for Traefik plus `sandboxd`. The control-plane API is published at `SANDBOXED_API_BIND` (default `127.0.0.1:9090`); preview traffic enters Traefik on `HTTP_PORT` (default `80`).

## Prerequisites

| Requirement | Details |
|---|---|
| Host OS | Linux with a standard Docker Engine daemon |
| Compose | Compose v2 plugin (`docker compose`) preferred; `docker-compose` standalone is accepted as a fallback |
| Docker access | Your user can run `docker info`, or `install.sh` falls back to `sudo docker` |
| Git checkout | Clone the repository; the installer only modifies files inside the repo and under `SANDBOXED_DATA_DIR` |

<Warning>
sandboxed targets a **single Docker host**. The control plane shells out to the host daemon over `/var/run/docker.sock` (mounted into both Traefik and `sandboxd`). macOS or Windows Docker Desktop is not the supported install path in this repository.
</Warning>

No Go toolchain is required on the host: the sandbox base image compiles `runtimed` inside Docker, and the control-plane image compiles `sandboxd` with CGO for SQLite.

## Install command

<Steps>
<Step title="Clone the repository">

```bash
git clone https://github.com/tastyeffectco/sandboxes.git
cd sandboxes
```

</Step>

<Step title="Run the installer">

```bash
./install.sh
```

The script is idempotent: re-running leaves an existing `.env` untouched, rebuilds images as needed, and restarts the compose stack.

</Step>

<Step title="Verify health endpoints">

```bash
curl -s http://127.0.0.1:9090/healthz
curl -s http://127.0.0.1:9090/readyz
```

<Check>
Expected responses: `ok` from `/healthz` and `ready` from `/readyz` (both HTTP 200). Adjust the host/port if you changed `SANDBOXED_API_BIND` in `.env`.
</Check>

</Step>
</Steps>

On success, the installer prints the API bind address, preview URL pattern, a sample `POST /sandbox` command, and `docker compose logs -f sandboxd` for follow-up.

## What install.sh does

`install.sh` runs six phases in order:

1. **Docker / Compose detection** — Probes `docker info`; if the current user cannot reach the daemon, retries with `sudo docker`. Selects `docker compose` when available, otherwise `docker-compose`.
2. **`.env` bootstrap** — Copies `.env.example` → `.env` only when `.env` is missing; never overwrites an existing file.
3. **Environment load** — Sources `.env` to resolve `SANDBOXED_DATA_DIR`, `SANDBOXED_LOG_DIR`, and `SANDBOXED_IMAGE` (default `sandboxed-base:1.0.0`).
4. **Data directory** — Creates `SANDBOXED_DATA_DIR` and `SANDBOXED_LOG_DIR` (with `sudo` when the parent is not writable). Sets log dir mode `0777` so Traefik can write the access log.
5. **Base image build** — Invokes `image/build.sh` with the image tag suffix (for example `1.0.0`). First build typically takes several minutes; later runs use the Docker layer cache.
6. **Stack build and start** — Runs `docker compose build` then `docker compose up -d`.

<Note>
The installer never deletes workspaces or SQLite state. It only creates the configured data paths and starts compose services.
</Note>

## Stack layout after compose up

```mermaid
flowchart TB
  subgraph host["Linux host — Docker daemon"]
    subgraph compose["docker compose stack"]
      traefik["traefik:v3\nHTTP :80 → host HTTP_PORT"]
      sandboxd["sandboxd\nAPI :9000 → host SANDBOXED_API_BIND"]
    end
    sock["/var/run/docker.sock"]
    data["SANDBOXED_DATA_DIR\nworkspaces/ · state/ · log/"]
    traefik --> sock
    sandboxd --> sock
    sandboxd --> data
    traefik --> data
  end
  browser["Browser / API client"] --> traefik
  browser --> sandboxd
  sandboxd -.->|"docker run (runtime)"| sandbox["s-{ulid} containers\nfrom SANDBOXED_IMAGE"]
  sandbox --> traefik
```

| Service | Image / build | Host port mapping | Key mounts |
|---|---|---|---|
| `traefik` | `traefik:v3` | `${HTTP_PORT:-80}:80` | Docker socket (ro), `./traefik/`, `${SANDBOXED_LOG_DIR}` |
| `sandboxd` | `build: ./control-plane` → `sandboxed-control-plane:1.0.0` | `${SANDBOXED_API_BIND:-127.0.0.1:9090}:9000` | Docker socket, `${SANDBOXED_DATA_DIR}`, `${SANDBOXED_LOG_DIR}` |

Both services set `userns_mode: host` so infrastructure containers keep working when the daemon uses `userns-remap`. Per-sandbox containers are launched at runtime by `sandboxd`, not declared as compose services.

Sandboxes join the `${SANDBOXED_NETWORK:-sandboxed_net}` bridge network so Traefik can route preview hostnames to them.

## `.env` bootstrap

`install.sh` creates `.env` from `.env.example` when absent. All keys have defaults; an empty `.env` is valid.

Edit `.env` **before** re-running install (or restart the stack with `docker compose up -d` after changes). Keys most often adjusted on first install:

| Variable | Default | Effect |
|---|---|---|
| `HTTP_PORT` | `80` | Host port published for Traefik HTTP previews |
| `SANDBOXED_API_BIND` | `127.0.0.1:9090` | Where `sandboxd` is reachable on the host |
| `PREVIEW_DOMAIN` | `localhost` | Hostname suffix for preview URLs (`s-{id}-{port}.preview.{domain}`) |
| `SANDBOXED_DATA_DIR` | `/var/lib/sandboxed` | Workspaces, SQLite (`state/sandboxd.db`), and logs |
| `SANDBOXED_IMAGE` | `sandboxed-base:1.0.0` | Image tag passed to `image/build.sh` and sandbox `docker run` |
| `SANDBOXD_API_AUTH_DISABLED` | `true` | API open on loopback by default; set `false` + tokens for LAN exposure |

<ParamField body="SANDBOXED_DATA_DIR" type="absolute path" required>
Must be an absolute path. Compose bind-mounts the same host path into `sandboxd` so workspace paths written by the control plane resolve correctly when sibling sandboxes are created on the host daemon.
</ParamField>

Additional keys (`PREVIEW_ENTRYPOINT`, `PREVIEW_TLS`, `SANDBOXD_IDLE_THRESHOLD_SECONDS`, auth tokens, and cgroup toggles) are documented on the configuration reference page.

## Image builds

### Sandbox base image (`sandboxed-base`)

`image/build.sh` builds from the **repository root** (not `image/` alone) so the Dockerfile can compile `runtimed` from `control-plane/`:

```bash
DOCKER="${DOCKER:-docker}" SANDBOXED_IMAGE="${SANDBOXED_IMAGE:-sandboxed-base:1.0.0}" \
  bash image/build.sh 1.0.0
```

The Dockerfile uses two stages:

- **Stage 1** — `golang:1.22-bookworm` builds a static `runtimed` binary (`CGO_ENABLED=0`).
- **Stage 2** — `debian:stable-slim` installs Node, pnpm, uv, bun, Claude Code, OpenCode, and sets `CMD ["/usr/local/bin/runtimed"]` under `tini`.

Native architecture is built automatically (arm64 and amd64 hosts are supported). Override the tag with `SANDBOXED_IMAGE` or the version argument to `build.sh`.

### Control plane image (`sandboxd`)

`docker compose build` uses `control-plane/Dockerfile`:

- **Stage 1** — `CGO_ENABLED=1` build of `sandboxd` (SQLite via `mattn/go-sqlite3`).
- **Stage 2** — `debian:stable-slim` plus `docker-ce-cli`; migrations copied to `/usr/local/share/sandboxd/migrations/`.

Published compose image name: `sandboxed-control-plane:1.0.0`. Container listens on **9000** internally; the host mapping comes from `SANDBOXED_API_BIND`.

<Info>
You do not need Go installed on the host for either image. Rebuild only the control plane after API changes with `docker compose build sandboxd && docker compose up -d sandboxd`.
</Info>

## Health and readiness

After the stack is up, probe the control plane on the configured API bind:

:::endpoint GET /healthz
Liveness: process is serving HTTP. No dependency checks.

<ResponseExample>

```http
HTTP/1.1 200 OK

ok
```

</ResponseExample>
:::

:::endpoint GET /readyz
Readiness: SQLite is reachable **and** `docker info` succeeds against the mounted host socket.

<ResponseExample>

```http
HTTP/1.1 200 OK

ready
```

</ResponseExample>

On failure returns HTTP 503 with a JSON error body (`sqlite ping: …` or `docker info: …`). Auth middleware exempts both endpoints.
:::

<CodeGroup>

```bash title="healthz"
curl -s -w "\nHTTP %{http_code}\n" http://127.0.0.1:9090/healthz
```

```bash title="readyz"
curl -s -w "\nHTTP %{http_code}\n" http://127.0.0.1:9090/readyz
```

</CodeGroup>

| Endpoint | Pass signal | Typical failure |
|---|---|---|
| `/healthz` | Body `ok`, status 200 | Stack not started or wrong `SANDBOXED_API_BIND` |
| `/readyz` | Body `ready`, status 200 | Docker socket not mounted, daemon down, or SQLite path not writable |

<Warning>
`/healthz` succeeding while `/readyz` returns 503 usually means `sandboxd` is running but cannot talk to Docker or open its database under `SANDBOXED_DATA_DIR`. Check `docker compose logs sandboxd` and that `/var/run/docker.sock` is mounted in the `sandboxd` service.
</Warning>

## Post-install URLs

With defaults from `.env.example`:

| Surface | URL pattern |
|---|---|
| Control-plane API | `http://127.0.0.1:9090` |
| Preview (port 3000 example) | `http://s-{id}-3000.preview.localhost` (append `:${HTTP_PORT}` when `HTTP_PORT` ≠ `80`) |

Browsers resolve `*.localhost` to `127.0.0.1` without extra DNS. For a first sandbox, omit `id` in `POST /sandbox` to receive an auto-generated ULID.

## Common install adjustments

<Tabs>
<Tab title="Port 80 in use">

Set `HTTP_PORT=8088` (or another free port) in `.env`, then:

```bash
docker compose up -d
```

Preview URLs must include the port suffix, for example `http://s-{id}-3000.preview.localhost:8088`.

</Tab>

<Tab title="Expose API on LAN">

Set `SANDBOXED_API_BIND=0.0.0.0:9090` and enable token auth:

```env
SANDBOXD_API_AUTH_DISABLED=false
SANDBOXD_API_TOKENS=myapp:your-secret-token
```

Clients must send `Authorization: Bearer your-secret-token` on protected routes.

</Tab>

<Tab title="Custom data directory">

Set `SANDBOXED_DATA_DIR` and matching `SANDBOXED_LOG_DIR` in `.env` before install. The directory must be absolute and writable (installer uses `sudo` when needed).

</Tab>
</Tabs>

## Manual compose (without install.sh)

Equivalent steps if you manage the process yourself:

```bash
cp .env.example .env    # if missing
bash image/build.sh 1.0.0
docker compose build
docker compose up -d
```

Ensure `SANDBOXED_DATA_DIR` and `SANDBOXED_LOG_DIR` exist with Traefik log dir permissions before `up`.

## Install troubleshooting

| Symptom | Likely cause | Action |
|---|---|---|
| `Docker is not available` | Daemon stopped or not installed | Start Docker Engine; add user to `docker` group or rely on installer's `sudo docker` path |
| `Docker Compose not found` | Missing Compose plugin | Install `docker-compose-plugin` (Compose v2) |
| `/readyz` → `docker info: exit status 1` | Socket not reachable from `sandboxd` container | Confirm `docker compose ps`, daemon running, socket mount in `docker-compose.yml` |
| Preview URLs timeout locally | Wrong port or domain | Match `HTTP_PORT` and `PREVIEW_DOMAIN`; use `Host` header when curling Traefik directly |
| Base image build slow | Cold cache | Expected on first run; subsequent `install.sh` runs reuse layers |
| Workspace seed errors on create | `userns-remap` on daemon | Keep default `SANDBOXED_USERNS=host` (compose passes it to `sandboxd`) |

```bash
docker compose logs -f sandboxd
docker compose ps
```

## Related pages

<CardGroup>
<Card title="Overview" href="/overview">
What the stack exposes after install: API, Traefik previews, and the shortest create → task → preview path.
</Card>
<Card title="Quickstart" href="/quickstart">
Copy-paste sandbox creation, agent task submission, SSE events, and opening a preview URL.
</Card>
<Card title="Configuration reference" href="/configuration-reference">
Full `.env` / compose environment keys beyond the install-time subset.
</Card>
<Card title="Troubleshooting" href="/troubleshooting">
Deeper diagnosis for readyz failures, port conflicts, ULID validation, and warming-page stalls.
</Card>
<Card title="Uninstall and maintenance" href="/uninstall-maintenance">
`uninstall.sh` flags, workspace retention, and stack maintenance commands.
</Card>
</CardGroup>
