# Installing Maxy Code on a Raspberry Pi

End-to-end install for a fresh Raspberry Pi 5 (16GB) on Ubuntu Server 24.04 (64-bit). Every command is copy-pasteable and uses auto-yes flags so nothing prompts interactively. The same flow works on a Pi 4 (8GB). For a Hetzner Cloud install (CAX31 ARM64 ~€13/mo), see [hetzner.md](hetzner.md) — same installer, slightly different bootstrap for the Cloudflare tunnel because there is no LAN to the operator.

The doc is brand-aware. Examples use the default brand `maxy-code`; substitute `realagent-code` (or any other brand under `maxy-code/brands/`) wherever you want a parallel install. Each brand is fully isolated — its own persist directory, its own systemd user-service, its own Neo4j port, its own VNC display, its own Cloudflare tunnel, its own `CLAUDE_CONFIG_DIR`.

> macOS install: see [macos.md](macos.md) for the laptop flow.
> Architecture notes for engineers: see [../deployment.md](../deployment.md).

## Requirements

- Raspberry Pi 5, 16GB RAM (canonical) — Pi 4 8GB works but the first install runs slower.
- Ubuntu Server 24.04 LTS, 64-bit, freshly imaged with Raspberry Pi Imager. Earlier Ubuntu / Pi OS releases are not part of the supported matrix.
- The pi has a wired or Wi-Fi route to the internet and an SSH-reachable user with sudo (the username does not matter — Rubytech images ship `admin` by default).
- A Cloudflare account whose dashboard you can sign into in a web browser. No API tokens are ever issued or stored; the only Cloudflare auth path is `cloudflared tunnel login` running in the Pi's VNC browser after install.
- A connected monitor or a working VNC viewer for the one-time `cloudflared tunnel login` step. After that step the Pi runs headless.

For Hetzner Cloud, see [hetzner.md](hetzner.md). The apt path, systemd user-service, and Cloudflare flow are the same; the difference is that a cloud VM has no physical display and no LAN to the operator, so the noVNC browser is reached over SSH port-forwarding for the one-time Cloudflare bootstrap.

## 1. Prepare the OS

Update the package index and install Node 22 from NodeSource. Pi OS / Ubuntu archive Node is too old; the installer reads `node` from `PATH` and a 20.x binary trips the engines check.

```bash
sudo apt-get update
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
sudo apt-get install -y nodejs

# Verify (must be 22.6 or newer)
node --version
```

Everything else the installer needs (apt deps for the VNC stack, `cloudflared`, Neo4j, Ollama, Chromium) is installed by `@rubytech/create-maxy-code` in step 2 — do not pre-install them by hand.

## 2. Run the installer

The default brand is `maxy-code`. Run as the same user that will operate the device (do not run with `sudo`; the installer escalates internally where it needs to). The `--hostname` flag is required on Pi and cloud VM — it becomes the Cloudflare-fronted hostname and the systemd unit name, and it is the hostname the LAN sees over mDNS.

```bash
npx -y @rubytech/create-maxy-code@latest --hostname <hostname>
```

Pick a `<hostname>` that is short, lowercase, and unique across your Cloudflare account (e.g. `maxy-alice`). The installer sets `HostName`, `LocalHostName`, and the Avahi `host-name` to this value, then registers a systemd user-service named `<hostname>.service` that owns the platform process.

That command:

- creates the persist directory `$HOME/.maxy-code/` (logs, config, plugin state, the `.claude/` config tree, browser profile);
- exports `CLAUDE_CONFIG_DIR=$HOME/.maxy-code/.claude` for every Claude Code invocation it spawns (default `~/.claude` is the wrong tree on a multi-brand machine);
- `apt-get install -y` for the base deps, the VNC stack (`tigervnc-standalone-server`, `python3-websockify`, `novnc`, `xdg-utils`, `chromium`, `xterm`, `xdotool`), `cloudflared`, Neo4j 5.x, and `nodejs`;
- swaps a snap-Chromium for a deb-packaged Chromium (or Google Chrome) when the Ubuntu image ships Chromium as a snap — snap-confined Chromium cannot run inside the VNC display;
- builds the platform payload bundled in the npm tarball;
- writes a systemd user-service at `~/.config/systemd/user/<hostname>.service` and enables it with `systemctl --user enable --now`;
- prints the LAN URL `http://<hostname>.local:<port>` when the supervisor reports the server is listening. The Cloudflare-fronted public URL is not provisioned at install time — step 4 below.

The full install log lands at `$HOME/.maxy-code/logs/install-<timestamp>.log`. Every phase line is prefixed `[create-maxy] phase=… brand=… platform=linux` — that is the canonical signal if you want to attach an install log to a support request.

If `~/.maxy-code/logs/install-*.log` is empty after a failed install, grep the installer's stdout for `[create-maxy] platform=`, `[create-maxy] log=`, and `[create-maxy] init-logging FAILED reason=`. The installer emits those to stdout (and stderr for the last one) before any log file write.

### Installing a second brand

To run, for example, `realagent-code` alongside the default install on the same Pi, repeat step 2 with that brand's package and a different hostname:

```bash
npx -y @rubytech/create-realagent-code@latest --hostname <realagent-hostname>
```

The persist directory becomes `$HOME/.realagent-code`, the systemd user-service becomes `<realagent-hostname>.service`, Neo4j is provisioned as a dedicated `neo4j-<realagent-hostname>` service on its own port, and the VNC display + websockify + ttyd ports all shift to the brand's reserved range. There is no shared state; `CLAUDE_CONFIG_DIR` is always `$HOME/.<brand>/.claude` for that brand, never the default `~/.claude`.

## 3. Confirm the systemd user-service is up

```bash
systemctl --user status <hostname>.service
```

You should see `Active: active (running)`. If the unit is in `failed` or `activating` state, tail the supervised journal:

```bash
journalctl --user -u <hostname>.service -n 200 --no-pager
```

The unit reads its environment from `$HOME/.maxy-code/.env`; if you edited that file by hand and broke a quoted value, the supervisor will respawn on a fast loop and the LAN URL never becomes reachable.

The installer also wires `loginctl enable-linger <user>` so the user-service survives logout. If `loginctl show-user <user> | grep Linger` does not return `Linger=yes`, re-run the installer or `sudo loginctl enable-linger <user>` by hand — without linger the service stops when you log out of the Pi.

## 4. Bootstrap the Cloudflare tunnel

The installer puts `cloudflared` on PATH but does not provision the tunnel — Cloudflare auth happens once, interactively, in a browser the operator drives. There is no API token, no service token, no SDK call: the only auth path is `cloudflared tunnel login`, which writes a browser-issued cert to `$HOME/.maxy-code/.cloudflared/cert.pem` on success.

Open the Pi's VNC browser at `http://<hostname>.local:<port>/vnc` (or over the LAN at whichever port the install log printed for noVNC). In the chat surface, ask the agent to run the Cloudflare setup — the [`cloudflare`](../../platform/plugins/cloudflare/PLUGIN.md) plugin's `setup-tunnel` skill walks `cloudflared tunnel login`, `cloudflared tunnel create`, `cloudflared tunnel route dns`, and the systemd `<hostname>-cloudflared.service` unit in order, streaming `cloudflared`'s stdout verbatim into chat. The OAuth URL the CLI prints is linkified by the PTY; the operator clicks it inside the VNC browser and authorises the cert against the right Cloudflare account.

Setup is done when, and only when, `curl -I https://<hostname>.<your-zone>` issued from outside the local network returns `HTTP/2 200`. No state file, no `tunnel run` exit code, and no "service is active" claim substitutes for the live HTTPS response.

## 5. Open the admin UI

After step 4 the public URL is your Cloudflare-fronted hostname. Open it in any browser, sign in, and the admin UI loads.

On the LAN (or pre-tunnel), the URL is `http://<hostname>.local:<port>` — the install log's final block prints both addresses:

```
================================================================

  Open in your browser: http://<hostname>.local:<port>
  Public URL (after Cloudflare setup): https://<hostname>.<your-zone>

================================================================
```

## 6. Verify reboot persistence

Reboot the Pi (`sudo reboot`). After the boot completes, the systemd user-service reattaches automatically because the unit is enabled and `loginctl enable-linger` was set. Re-open the LAN or public URL — it should respond within ten or twenty seconds without you doing anything.

If the admin UI does not respond after reboot:

- `systemctl --user status <hostname>.service` — confirm `active (running)`.
- `journalctl --user -u <hostname>.service -n 200 --no-pager` — tail the supervisor log.
- `loginctl show-user <user> | grep Linger` — confirm `Linger=yes`. Without it the user-service does not start until you SSH in.
- `systemctl --user status <hostname>-cloudflared.service` — confirm the tunnel is up. The platform unit can be healthy while the tunnel is not, in which case the LAN URL works and the public URL does not.

## Uninstall

```bash
npx -y @rubytech/create-maxy-code@latest --uninstall
```

This stops and disables the systemd user-service, removes the unit file, removes the Avahi service file, removes the brand's `sysctl.d` QUIC-tuning file, and removes the persist directory `$HOME/.maxy-code/`. Shared apt packages (Node, Neo4j, Chromium, the VNC stack, `cloudflared`) stay on the system — the operator removes them with `sudo apt-get purge` if they want a clean slate.

To uninstall a non-default brand, point at its package — for example:

```bash
npx -y @rubytech/create-realagent-code@latest --uninstall
```

## What this install does not do

- **No SCP / rsync.** The Pi is reached over npm only. Updates are `npx -y @rubytech/create-maxy-code@latest …` again, never a file push from the operator's laptop.
- **No Cloudflare API tokens.** The only Cloudflare auth path is `cloudflared tunnel login` running in the Pi's VNC browser. If a doc, plugin, or workflow asks for a CF API token it is wrong — surface the discrepancy before proceeding.
- **No shared state across brands.** Two brands on one Pi each have their own Neo4j port, systemd unit, VNC display, websockify port, tunnel, and persist directory. They do not share DNS, ports, or filesystem state.

## Smoke checklist

Fresh-Pi smoke pass criteria:

1. Install on a clean Ubuntu Server 24.04 image with no prior Maxy footprint completes, prints a LAN URL, and the systemd user-service is `active (running)`.
2. The LAN URL `http://<hostname>.local:<port>` opens the admin UI and the chat surface is interactive.
3. Cloudflare setup driven by the `cloudflare` plugin's `setup-tunnel` skill ends with `curl -I https://<hostname>.<your-zone>` returning `HTTP/2 200` from outside the LAN.
4. Reboot — both URLs are reachable again after boot without any manual action.
5. Install a second brand with a different `--hostname`; both brands' admin UIs are reachable on their own ports / public URLs and neither has touched the other's state.
6. Uninstall removes the systemd unit, the Avahi service file, and the persist directory.

If any step fails, attach `$HOME/.<brand>/logs/install-<timestamp>.log` to the report.
