MaxyDocs
View as markdown →

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 — 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 for the laptop flow. Architecture notes for engineers: see ../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. 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.

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.

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:

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

systemctl --user status <hostname>.service

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

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 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

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:

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.