---
title: Single-Node k3s Quick Deploy
description: Bring up the full Neutree Agent Platform on one machine with k3s — the quickest install path, connected or fully air-gapped.
---

import AirgapBlock from '../../../components/self-host/AirgapBlock.tsx'

Install Neutree Agent Platform on a single-node k3s cluster — the quickest way to get the full platform running. The single-node profile matches the full one, just with a single PostgreSQL instance and an in-cluster NFS server for shared storage.

The same flow serves two kinds of nodes. **Connected**, every image is pulled from the public registry (`ghcr.io` / `docker.io` / `registry.k8s.io`). For a node with **no internet access**, the extra steps sit inline in collapsible **Air-gapped** blocks — expand one and they all stay open; that path installs from an image bundle via an in-cluster registry, so nothing is pulled from the internet.

Recommended spec: **8 vCPU / 32 GB / 200 GB disk** — comfortable for roughly **10 concurrent workspaces**. When you need HA or horizontal scale, use the [production deployment](/nap/self-host/), which also covers [installing into an existing Kubernetes cluster](/nap/self-host/#install).

## One-line install (connected)

On a bare Linux machine with internet access, k3s is **not** required — the script installs it. Run as root:

```bash
curl -sfL https://docs.neutree.ai/nap/get.sh | sudo sh -
```

It installs k3s if missing, generates every secret, autodetects the node IP, creates a random admin password, and prints the login URL + credentials when it finishes. Configuration lands in `/opt/nap/values.env`; re-running the same line upgrades in place.

Common overrides:

```bash
# choose the host IP / admin password yourself
curl -sfL https://docs.neutree.ai/nap/get.sh \
  | sudo NAP_HOST=192.168.1.10 NAP_ADMIN_PASSWORD=your-password sh -

# generate values.env only — review/edit it, then re-run without the flag to install
curl -sfL https://docs.neutree.ai/nap/get.sh \
  | sudo sh -s -- --prepare-only
```

`get.sh --help` lists all flags (`--version=` to pin a release, `--dir=` to relocate the install dir). An air-gapped node can't run the one-liner — follow the manual flow below.

## Manual install

The same steps the one-liner automates, under your control — and the path for air-gapped nodes. Run everything on the node itself.

### 1. Get the materials

```bash
git clone https://github.com/neutree-ai/agent-platform
cd agent-platform/self-host
```

If you already ran the one-liner (even with `--prepare-only`), the same directory is at `/opt/nap/self-host` — work there instead.

<AirgapBlock client:load locale="en" summary="build two bundles on a connected host, then stage them on the node">

On any internet-connected machine, from the `self-host/` directory:

```bash
./offline/save-images.sh                         # → offline/nap-images.tar.gz + prereq charts
./single-node-prep/package-prep.sh --arch amd64  # → k3s + helm/envsubst/crane + airgap images
```

`save-images.sh` writes `offline/nap-images.tar.gz` (every first-party and third-party image, plus `registry:2`) and the CNPG / NFS-provisioner charts under `prereqs/`. `package-prep.sh` bundles the k3s binary, its airgap images, and the CLI tools `install.sh` depends on — pick the `--arch` (`amd64` / `arm64`) that matches the target node. If a vendor delivered these bundles, skip this step.

Copy the `self-host/` directory (with `offline/nap-images.tar.gz` inside) and the prep tarball to the air-gapped node, and extract the prep tarball **inside `self-host/`**, so it lands at `self-host/single-node-prep/` — that's where `preinstall.sh` expects to find `../offline/nap-images.tar.gz` (extracting it as a sibling of `self-host/` also works; the script checks both layouts).

</AirgapBlock>

### 2. Prepare values.env

```bash
cp values.env.single-node.example values.env
./gen-secrets.sh        # fills random JWT / PG / TURN / SANDBOX secrets
vi values.env           # set NAP_HOST + ADMIN_PASSWORD
```

`values.env.single-node.example` already has single-node defaults baked in, so you only need to change two things:

- `NAP_HOST` — the node's externally reachable IP; pods and the login entry both use it
- `ADMIN_PASSWORD` — the initial admin password

Everything else (PG replica count, storage classes, NFS backend) is preset for the single-node shape.

<AirgapBlock client:load locale="en" summary="NAP_HOST must be set before the next step">

Set `NAP_HOST` **before** step 3 — `preinstall.sh` writes the containerd config that lets the node reach the in-cluster registry over HTTP, and it needs the host.

</AirgapBlock>

### 3. Prepare the node (k3s)

The install needs a working k3s with its kubeconfig at `/etc/rancher/k3s/k3s.yaml` (the default). If the node doesn't have k3s yet:

```bash
curl -sfL https://get.k3s.io | sh -
```

<AirgapBlock client:load locale="en" summary="preinstall.sh installs k3s and imports the images — no internet">

Instead of the online k3s installer, run:

```bash
cd single-node-prep
sudo ./preinstall.sh
cd ..
```

This installs k3s from the bundle, drops in `helm` / `envsubst` / `crane`, imports `nap-images.tar.gz` into k3s containerd (priming `registry:2` and the third-party images), and configures containerd to treat the in-cluster registry as plain HTTP. Re-run it any time after editing `values.env`.

</AirgapBlock>

### 4. Install

```bash
./install.sh --profile=single-node
```

`install.sh` first brings up the in-cluster NFS server (`nap-nfs-server` pod, backed by `local-path`), installs the CloudNativePG operator and the NFS subdir provisioner, renders and applies the manifests, then seeds the admin user, OAuth clients, and the MCP catalog. Connected, all images are pulled from the public registry.

<AirgapBlock client:load locale="en" summary="the image bundle switches install.sh to the in-cluster registry">

Because `offline/nap-images.tar.gz` is present, `install.sh` brings up the in-cluster `nap-registry`, seeds it from the bundle, and points every workload at it — no external registry, no internet. Everything else matches the connected flow.

</AirgapBlock>

### 5. Log in

```
http://<NAP_HOST>:30080
admin / <ADMIN_PASSWORD>
```

`30080` is the `NAP_NODE_PORT` default baked into `values.env.single-node.example`.

## Key differences from the standard deploy

Only the values that differ from the [deployment guide](/nap/self-host/) defaults are listed; everything else carries over:

| Field | Standard default | Single-node |
| --- | --- | --- |
| `DEPLOY_PROFILE` | `multi-node` (implicit) | `single-node` |
| `PG_INSTANCES` | 3 | 1 |
| `PG_STORAGE_CLASS` | any RWO CSI | `local-path` |
| `AGENT_STORAGE_CLASS` | NFS / RWX CSI | `local-path` |
| `NFS_SERVER` / `NFS_PATH` | external NFS | in-cluster NFS pod (`nap-nfs-server`), computed by `install.sh` |

All of these are already written into `values.env.single-node.example`, so you don't fill them by hand.

:::caution[NFS kernel modules must be enabled on the host]
The in-cluster NFS uses the host kernel: the `nap-nfs-server` pod mounts `nfsd` on startup, and the nfs-subdir provisioner needs the `nfs` client module to mount. If the modules are missing, the `nap-nfs-server` pod won't come up and the AFS PVC stays `Pending`.

Check whether they're loaded:

```bash
lsmod | grep -E 'nfs|nfsd'
```

If there's no output, load both manually:

```bash
sudo modprobe nfs
sudo modprobe nfsd
lsmod | grep -E 'nfs|nfsd'   # confirm loaded
```

If `modprobe` reports `module not found`, the kernel lacks the NFS module package — install it first (e.g. `apt-get install linux-modules-extra-$(uname -r)`) or switch to a kernel image with NFS support, then re-run the install.
:::

## Notes

- **Data lives on the node** — PVCs (including AFS shared files) use `local-path`, bound to the node's disk. Back up externally if the data matters
- **Run the installer on the node itself** — the single-node profile pulls images into the local cluster
- **Upgrades restart pods** — expect a brief window while images roll. Connected: re-run the one-liner, or update the repo (`git pull`) and re-run `./install.sh --profile=single-node`

<AirgapBlock client:load locale="en" summary="upgrading an air-gapped node needs fresh bundles">

On the connected host, rebuild the image bundle from the new release (`./offline/save-images.sh`; the prep tarball usually doesn't change), copy the new `self-host/` directory with the bundle to the node, re-run `sudo ./preinstall.sh` to import the new images, then `./install.sh --profile=single-node`.

</AirgapBlock>

## What's next

You're logged in as the admin of a fresh, empty platform. Two short guides take you from here to a working agent:

1. **[Set up an API provider](/nap/guides/1-setup/)** — connect a large-model API so agents can think. On a fresh install the provider list is empty; creating the first one takes a minute.
2. **[Your first agent](/nap/guides/2-first-agent/)** — create a workspace and finish your first conversation, in under 5 minutes.

From there, [concepts](/nap/concepts/overview/) explains the moving parts, and the remaining guides cover triggers, multi-agent collaboration, and operating at scale.
