# Install

This is the shortest production-like path from zero to a running self-hosted Sandbox0.

## Prerequisites

- Kubernetes `1.35+`
- Helm `3.8+`
- `kubectl` matching your cluster minor version
- A default `StorageClass`

<Callout variant="info">
Kubernetes `1.35+` is required for the supported Sandbox0 runtime stack. Checkpointed pause/resume depends on `ctld` on sandbox nodes so Sandbox0 can save and restore writable rootfs checkpoints when releasing and recreating runtime pods.
</Callout>

## 0) Create a Local Kind Cluster

Create the cluster:

```bash
kind create cluster --config kind-config.yaml
```

`kind-config.yaml`:

```yaml
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
name: sandbox0
nodes:
- role: control-plane
  image: kindest/node:v1.35.0
  kubeadmConfigPatches:
  - |
    kind: ClusterConfiguration
    apiServer:
      extraArgs:
        enable-aggregator-routing: "true"
  extraPortMappings:
  # cluster-gateway HTTP port
  - containerPort: 30080
    hostPort: 30080
  # registry port for template image push
  - containerPort: 30500
    hostPort: 30500
```

## 1) Install infra-operator

```bash
helm repo add sandbox0 https://charts.sandbox0.ai
helm repo update

helm install infra-operator sandbox0/infra-operator \
    --namespace sandbox0-system \
    --create-namespace
```

Verify operator + CRD:

```bash
kubectl get pods -n sandbox0-system
kubectl get crd sandbox0infras.infra.sandbox0.ai
```

## 2) Choose a Deployment Mode and Apply a Sample

Pick one official sample from source based on your target mode:

- <GitHubRawLink repo="sandbox0" path="infra-operator/chart/samples/single-cluster/minimal.yaml">single-cluster/minimal.yaml</GitHubRawLink>
- <GitHubRawLink repo="sandbox0" path="infra-operator/chart/samples/single-cluster/volumes.yaml">single-cluster/volumes.yaml</GitHubRawLink>
- <GitHubRawLink repo="sandbox0" path="infra-operator/chart/samples/single-cluster/network-policy.yaml">single-cluster/network-policy.yaml</GitHubRawLink>
- <GitHubRawLink repo="sandbox0" path="infra-operator/chart/samples/single-cluster/fullmode.yaml">single-cluster/fullmode.yaml</GitHubRawLink>
- <GitHubRawLink repo="sandbox0" path="infra-operator/chart/samples/multi-cluster/control-plane.yaml">multi-cluster/control-plane.yaml</GitHubRawLink>
- <GitHubRawLink repo="sandbox0" path="infra-operator/chart/samples/multi-cluster/data-plane.yaml">multi-cluster/data-plane.yaml</GitHubRawLink>

Apply the one you selected:

<Callout variant="info">
Default recommended mode: `fullmode` (Linux nodes only). For local macOS/Windows testing, use `single-cluster/minimal.yaml` instead.
</Callout>

<GitHubApplyCommand repo="sandbox0" path="infra-operator/chart/samples/single-cluster/fullmode.yaml" />

Watch status:

```bash
kubectl get sandbox0infra -n sandbox0-system -w
```

Expected `status.phase` lifecycle:

1. `Installing`
2. `Upgrading` (when reconciling changes)
3. `Ready`

## 3) Initial Admin Credentials

When local password bootstrap is enabled, if `spec.initUser.passwordSecret` is not provided, operator generates `admin-password`.

When built-in auth is enabled, the gateway bootstraps the initial admin with that password on first start.
When built-in auth is disabled but OIDC is enabled, the operator skips generating the password secret and the gateway still bootstraps the admin user and initial team,
but leaves the account passwordless so the first OIDC login for the same email can bind to that admin account.

```bash
ADMIN_PASSWORD="$(kubectl get secret admin-password -n sandbox0-system -o jsonpath='{.data.password}' | base64 -d)"
printf 'username: %s\npassword: %s\n' 'admin@example.com' "$ADMIN_PASSWORD"
```

## 4) Install `s0`

macOS and Linux:

```bash
curl -fsSL https://raw.githubusercontent.com/sandbox0-ai/s0/main/scripts/install.sh | bash
```

Windows PowerShell:

```powershell
irm https://raw.githubusercontent.com/sandbox0-ai/s0/main/scripts/install.ps1 | iex
```

Or with Go:

```bash
go install github.com/sandbox0-ai/s0/cmd/s0@latest
```

Manual release archives are available from <DocLink href="https://github.com/sandbox0-ai/s0/releases/latest" newTab>GitHub Releases</DocLink>. See the <DocLink href="https://github.com/sandbox0-ai/s0#installation" newTab>s0 README</DocLink> for platform-specific manual install steps.

## 5) Configure the API URL, Log In, and Optionally Create an API Key

For the local `kind` setup above, the API endpoint is `http://localhost:30080`.

That is because the `kind` config maps host port `30080`, and the sample `Sandbox0Infra` exposes `cluster-gateway` on the same NodePort.

Export the base URL, then log in with the initial admin account:

```bash
export SANDBOX0_BASE_URL="http://localhost:30080"

s0 auth login
```

At that point, interactive `s0` CLI usage is ready.

Create an API key only if you want to use the SDKs or raw HTTP API:

```bash
# If the server did not auto-select a team yet:
# s0 team list
# s0 team use <team-id>

unset SANDBOX0_TOKEN
export SANDBOX0_TOKEN="$(s0 apikey create --name sdk-quickstart --role developer --expires-in 30d --raw)"
```

After that, continue with [Get Started](/docs/sandbox/get-started) to make your first Sandbox0 API request.

## Advanced: Deploy on GCP

For Google Cloud, use GKE Standard rather than GKE Autopilot.

Recommended path:

1. Create a GKE Standard **regional** cluster with Linux nodes.
2. Pin Kubernetes to an explicit `1.35+` version. Do not rely on the GKE default version.
3. Keep a regular node pool for host-level system components.
4. Add a GKE Sandbox (`gVisor`) node pool for sandbox workloads.
5. Install `infra-operator`.
6. Apply the GCP `gVisor` sample.
7. Expose `cluster-gateway` through a `LoadBalancer`.
8. Verify the deployment by logging in with `s0` and claiming a sandbox.

### 1) Pick a Supported GKE Version

Sandbox0 requires Kubernetes `1.35+`.

Before creating the cluster, query the versions available in your region:

```bash
gcloud container get-server-config \
    --zone us-east1-b \
    --format='yaml(validMasterVersions)'
```

Choose any available `1.35.x` version from `validMasterVersions`, then reuse it below as `${GKE_VERSION}`.

### 2) Create a Regional Cluster

This creates a three-zone regional cluster in `us-east1`, with one regular Linux node per zone:

```bash
export GKE_VERSION="1.35.1-gke.1616000"

gcloud container clusters create sandbox0-gke-gvisor \
    --region us-east1 \
    --node-locations us-east1-b,us-east1-c,us-east1-d \
    --machine-type e2-standard-2 \
    --disk-size 30 \
    --num-nodes 1 \
    --cluster-version "${GKE_VERSION}"
```

Get kubeconfig and confirm the nodes:

```bash
gcloud container clusters get-credentials sandbox0-gke-gvisor --region us-east1
kubectl get nodes -o wide
```

Keep a regular node pool for core system services. In the current GKE `gVisor` sample, components such as `cluster-gateway`, `manager`, `storage-proxy`, PostgreSQL, and the builtin registry run on the regular pool, while sandbox workloads run on the `gVisor` pool.

### 3) Add a `gVisor` Node Pool

Add one `gVisor` node per zone:

```bash
gcloud container node-pools create gvisor-lite \
    --cluster sandbox0-gke-gvisor \
    --region us-east1 \
    --machine-type e2-standard-2 \
    --disk-size 30 \
    --num-nodes 1 \
    --sandbox type=gvisor
```

Verify both pools:

```bash
kubectl get nodes -L cloud.google.com/gke-nodepool,sandbox.gke.io/runtime
```

Expected result:

1. `default-pool` nodes have no `sandbox.gke.io/runtime` label
2. `gvisor-lite` nodes show `sandbox.gke.io/runtime=gvisor`

### 4) Install `infra-operator`

```bash
helm repo add sandbox0 https://charts.sandbox0.ai
helm repo update

helm install infra-operator sandbox0/infra-operator \
    --namespace sandbox0-system \
    --create-namespace
```

Verify operator + CRD:

```bash
kubectl get pods -n sandbox0-system
kubectl get crd sandbox0infras.infra.sandbox0.ai
```

### 5) Apply the GCP `gVisor` Sample

Use the official `gVisor` sample, then switch `cluster-gateway` to `LoadBalancer`:

<GitHubApplyCommand repo="sandbox0" path="infra-operator/chart/samples/single-cluster/fullmode-gke-gvisor.yaml" />

```bash
kubectl patch sandbox0infra fullmode -n sandbox0-system --type merge -p \
    '{"spec":{"services":{"clusterGateway":{"service":{"type":"LoadBalancer","port":80}}}}}'
```

Wait for the deployment to finish reconciling:

```bash
kubectl get sandbox0infra -n sandbox0-system -w
kubectl get svc -n sandbox0-system
```

Expected result:

1. `Sandbox0Infra` becomes `Ready`
2. `fullmode-cluster-gateway` is exposed as `LoadBalancer` on port `80`
3. `fullmode-netd` and `fullmode-ctld` run on `gvisor-lite`
4. sandbox pods in `tpl-default` run on `gvisor-lite`

This sample uses the native GKE node label `sandbox.gke.io/runtime=gvisor` for sandbox placement. You do not need to add a separate custom node label just to target `gVisor` nodes.

### RuntimeClass and Rootfs Clean/Restore

`services.manager.config.sandboxRuntimeClassName` only tells Sandbox0 which Kubernetes `RuntimeClass` to put on sandbox Pods. It does not install `runsc`, edit the node's containerd config, restart containerd, or create a custom runtime handler.

On GKE Sandbox, Google manages the standard `gVisor` runtime setup and the sample uses `sandboxRuntimeClassName: gvisor`. On self-managed clusters, install the runtime handler and `RuntimeClass` before pointing Sandbox0 at it.

For `gVisor` rootfs checkpoint and restore support, use a dedicated handler with shared rootfs access. `gvisor-rootfs` is just an example name; the Kubernetes `RuntimeClass.handler` must match the handler configured in containerd on every sandbox node.

```toml
# /etc/containerd/config.toml
version = 2

[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.gvisor-rootfs]
    runtime_type = "io.containerd.runsc.v1"

[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.gvisor-rootfs.options]
    TypeUrl = "io.containerd.runsc.v1.options"
    ConfigPath = "/etc/containerd/runsc-rootfs.toml"
```

```toml
# /etc/containerd/runsc-rootfs.toml
[runsc_config]
    overlay2 = "none"
    file-access = "shared"
```

Restart containerd after changing node runtime configuration, then create the matching `RuntimeClass`:

```yaml
apiVersion: node.k8s.io/v1
kind: RuntimeClass
metadata:
    name: gvisor-rootfs
handler: gvisor-rootfs
```

Then configure Sandbox0 sandbox Pods to use it:

```yaml
spec:
    services:
        manager:
            config:
                sandboxRuntimeClassName: gvisor-rootfs
```

Do not set `services.netd.runtimeClassName` to `gvisor`, `gvisor-rootfs`, or `kata`. `netd` should keep running on the node's default host-compatible runtime.

`ctld` assumes the node's containerd data root is `/var/lib/containerd` by default. If your node bootstrap stores containerd data somewhere else, set `services.ctld.containerdHostDataRoot` to the actual host path:

```yaml
spec:
    services:
        ctld:
            containerdHostDataRoot: /var/lib/containerd/sandbox0
```

For large writable rootfs checkpoints, ctld uses a disposable node-local object cache capped at `20Gi` per node by default. Tune the cap and free-space floor for your node disk:

```yaml
spec:
    services:
        ctld:
            rootfsObjectCacheMaxBytes: 20Gi
            rootfsObjectCacheMinFreeBytes: 5Gi
```

### 6) Expose the API Publicly

After the `LoadBalancer` patch above, GKE allocates a public IP for `cluster-gateway`.

Check the service:

```bash
kubectl get svc fullmode-cluster-gateway -n sandbox0-system -o wide
```

Wait until `EXTERNAL-IP` is no longer `<pending>`, then export the public base URL directly from the service:

```bash
export SANDBOX0_BASE_URL="http://$(kubectl get svc fullmode-cluster-gateway -n sandbox0-system -o jsonpath='{.status.loadBalancer.ingress[0].ip}')"
printf '%s\n' "$SANDBOX0_BASE_URL"
```

For production on GCP, this is a better default than exposing a node IP through `NodePort`.

### 7) Verify with `s0`

Retrieve the generated admin password:

```bash
ADMIN_PASSWORD="$(kubectl get secret admin-password -n sandbox0-system -o jsonpath='{.data.password}' | base64 -d)"
printf 'username: %s\npassword: %s\n' 'admin@example.com' "$ADMIN_PASSWORD"
```

Log in from the public endpoint:

```bash
s0 auth login \
    --api-url "${SANDBOX0_BASE_URL}" \
    --email admin@example.com \
    --password "${ADMIN_PASSWORD}"
```

Claim a sandbox to verify the deployment is actually usable:

```bash
s0 sandbox create --api-url "${SANDBOX0_BASE_URL}" -t default
```

Optional deeper check:

```bash
s0 sandbox list --api-url "${SANDBOX0_BASE_URL}"
s0 sandbox exec <sandbox-id> --api-url "${SANDBOX0_BASE_URL}" -- sh -lc 'echo ready && uname -s'
```

If `s0 sandbox create` returns a sandbox ID and the sandbox reaches `running`, the GCP deployment is working end to end.

### Important Notes

- Sandbox0 requires different runtime behavior for different components. On GKE, keep a regular node pool for system services and a `gVisor` node pool for sandbox workloads.
- Sandbox workload runtime selection should be handled by cluster defaults or administrator-managed service configuration, and the GCP `gVisor` sample places sandbox workloads onto nodes labeled `sandbox.gke.io/runtime=gvisor`.
- Use `sandboxNodePlacement` to place sandbox workloads, `netd`, and `ctld` onto the same sandbox node pool.
- Do not set `services.netd.runtimeClassName: gvisor`.
- `infra-operator` does not install containerd runtime handlers. Install `runsc`/`containerd-shim-runsc-v1`, configure the handler, restart containerd, and create the `RuntimeClass` before using a custom `sandboxRuntimeClassName`.
- If containerd uses a non-default data root, set `services.ctld.containerdHostDataRoot` to that host path so rootfs pause/resume can use the overlayfs fast path.
- `netd` and `ctld` depend on host features such as `hostNetwork` and `hostPath`. This is why GKE Autopilot is not a good fit for full Sandbox0 deployments.
- Even when `netd` and `ctld` are scheduled onto `gVisor` nodes, they still run on the node's default host runtime because `services.netd.runtimeClassName` remains unset.
- If you hit GCP `SSD_TOTAL_GB` quota limits while creating node pools, reduce `--disk-size` explicitly instead of relying on the larger default boot disk size.

### Why Not Autopilot

Autopilot is not suitable for the full `fullmode` deployment because Sandbox0 host-level components need capabilities that Autopilot restricts. In practice, `netd` and `ctld` need node-level access, while template sandbox pods run on the `gVisor` pool.

## Next Steps

<CardGroup>
  <Card title="Configuration" href="/docs/sandbox/self-hosted/configuration" cta="Continue">
    Tune topology, storage, networking, and service-level configuration.
  </Card>
</CardGroup>
