# Installing Maxy Code on a Hetzner Cloud server

End-to-end install for a fresh Hetzner Cloud server on the **CAX31** tier (8 vCPU Ampere Altra ARM64, 16 GB RAM, 160 GB NVMe, ~€13/mo). CAX is the right tier because it is ARM64, identical chip family to the Raspberry Pi 5, so every binary built by the installer compiles the same way it does on the Pi. Every command is copy-pasteable and uses auto-yes flags so nothing prompts interactively.

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

> Pi install: see [pi.md](pi.md). macOS install: see [macos.md](macos.md). Architecture notes for engineers: see [../deployment.md](../deployment.md).

> **Data sovereignty note.** Installing on Hetzner moves the operator's graph and conversations from a device they own onto a rented server. For internal use or for operators who explicitly prefer cloud hosting, fine. As the default for customers, this cuts against the inverted-SaaS positioning — surface the trade-off before recommending it.

## Server spec

| Field | Value | Why |
|---|---|---|
| Tier | **CAX31** | 8 vCPU, 16 GB RAM, 160 GB NVMe, ~€13/mo. RAM matches the Pi 16GB; ARM64 keeps binary compatibility. CAX11/21 are under-spec for the platform's Neo4j + Chromium + Ollama footprint. |
| Image | Ubuntu 24.04 LTS (arm64) | Same image family supported by the Pi install. Earlier Ubuntu / non-LTS images are not part of the supported matrix. |
| Location | Nearest to the operator (Falkenstein, Nuremberg, Helsinki, Hillsboro, Singapore) | Latency to the admin browser; choice does not affect the install. |
| Network | IPv4 + IPv6 | The Cloudflare tunnel terminates all public traffic; the server's own IPv4 is not exposed to operators after step 4. |
| Firewall | SSH (22) inbound only | Every other inbound surface is fronted by the Cloudflare tunnel, which dials *out* to Cloudflare. |
| SSH key | Added at provision time | Hetzner does not enable password SSH on the default Ubuntu image when an SSH key is attached. |

A CAX11 or CAX21 cannot run the platform. The Pi 16GB is the floor; CAX31 is the like-for-like Hetzner equivalent.

## 1. Provision the server

In the [Hetzner Cloud console](https://console.hetzner.cloud):

1. Create a project (or use an existing one).
2. Add server → **Location**: nearest region → **Image**: Ubuntu 24.04 → **Type**: Arm64 → **CAX31**.
3. Add your SSH key under **SSH keys** (or paste it inline). Skip the cloud-init / user-data field.
4. Name the server (e.g. `maxy-alice`) and create it.

When the server reaches `Running`, copy its public IPv4. SSH in as `root`:

```bash
ssh root@<ipv4>
```

## 2. Prepare the OS

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

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

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

Create a non-root user that will own the install and the systemd user-service. Running the platform as `root` is supported but not recommended; the rest of this doc assumes a user named `admin` (matching the Pi default).

```bash
adduser --disabled-password --gecos "" admin
usermod -aG sudo admin
echo "admin ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/admin
chmod 440 /etc/sudoers.d/admin
mkdir -p /home/admin/.ssh
cp ~/.ssh/authorized_keys /home/admin/.ssh/authorized_keys
chown -R admin:admin /home/admin/.ssh
chmod 700 /home/admin/.ssh
chmod 600 /home/admin/.ssh/authorized_keys

# From now on, SSH as admin, not root
ssh admin@<ipv4>
```

Everything else the installer needs (apt deps for the VNC stack, `cloudflared`, Neo4j, Ollama, Chromium) is installed by `@rubytech/create-maxy-code` in step 3.

## 3. Run the installer

The default brand is `maxy-code`. Run as the `admin` user (do not use `sudo`; the installer escalates internally where it needs to). The `--hostname` flag is required on a cloud VM — it becomes the Cloudflare-fronted hostname and the systemd unit name.

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

- creates the persist directory `$HOME/.maxy-code/` (logs, config, plugin state, `.claude/` config tree, browser profile);
- exports `CLAUDE_CONFIG_DIR=$HOME/.maxy-code/.claude` for every Claude Code invocation;
- `apt-get install -y` for 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 when the Ubuntu image ships Chromium as a snap;
- 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 loopback URL `http://localhost:<port>` when the supervisor reports the server is listening.

The full install log lands at `$HOME/.maxy-code/logs/install-<timestamp>.log`.

### Installing a second brand

Repeat step 3 with the other 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 on its own port, and the VNC display + websockify + ttyd ports shift to the brand's reserved range.

## 4. Reach the dashboard and VNC browser over SSH port-forwarding

On the Pi both the admin UI and the noVNC page are reachable over the LAN. On Hetzner there is no LAN to the operator, so both surfaces are forwarded over SSH until the Cloudflare tunnel exists.

All `ssh -L` commands in this step are run on **your local machine** — the machine you SSH from, not on the Hetzner server.

Both the dashboard and the VNC browser can be forwarded in a single SSH session using two `-L` flags. On your local machine, open one terminal and run:

```bash
ssh -L 19200:localhost:19200 -L 6080:localhost:6080 admin@<ipv4>   # maxy-code
# or
ssh -L 19200:localhost:19200 -L 6081:localhost:6081 admin@<ipv4>   # realagent-code
```

While that session is open:
- `http://localhost:19200` — dashboard
- `http://localhost:6080/vnc.html` — VNC browser (Claude's OAuth and Cloudflare setup run here)

The server-side ports are fixed by brand (`19200` dashboard, `6080`/`6081` VNC). When managing multiple servers simultaneously, vary only the left-hand (local) ports:

```bash
# One terminal per server, on your local machine
ssh -L 19200:localhost:19200 -L 6080:localhost:6080 admin@server1
ssh -L 19201:localhost:19200 -L 6081:localhost:6080 admin@server2
ssh -L 19202:localhost:19200 -L 6082:localhost:6080 admin@server3
```

After the Cloudflare tunnel is provisioned, close the SSH session — every surface is reachable at the public hostname.

## 5. Bootstrap the Cloudflare tunnel

The installer puts `cloudflared` on PATH but does not provision the tunnel — Cloudflare auth happens once, interactively, in the noVNC browser the operator drives over the SSH forward from step 4. 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.

In the noVNC browser session, open the admin UI at `http://localhost:<port>`. In chat, 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 noVNC 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 the operator's laptop returns `HTTP/2 200`. No state file, no `tunnel run` exit code, and no "service is active" claim substitutes for the live HTTPS response.

The SSH port-forward from step 4 can be closed after this point.

## 6. Open the admin UI

After step 5 the public URL is your Cloudflare-fronted hostname. Open it in any browser (laptop, phone, tablet), sign in, and the admin UI loads.

The Hetzner server's IPv4 is not advertised anywhere; the only public surface is the Cloudflare hostname. If the operator's laptop is offline, the loopback URL inside an SSH session (`http://localhost:<port>` over `ssh -L`) still works.

## 7. Verify reboot persistence

Reboot the server (`sudo reboot`). After it comes back up, SSH back in and confirm:

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

Both should be `Active: active (running)` within ten or twenty seconds of boot. `loginctl show-user admin | grep Linger` must report `Linger=yes` — without it the user-service does not start until you SSH in. The installer sets linger; if it is missing, run `sudo loginctl enable-linger admin`.

Open the public URL from outside the server's network and confirm the admin UI is reachable without any manual action.

## Uninstall

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

This stops and disables the systemd user-service, removes the unit 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. To wipe the box completely, destroy the Hetzner server from the cloud console.

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

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

## What this install does not do

- **No SCP / rsync.** 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` in the noVNC browser over SSH forward.
- **No shared state across brands.** Two brands on one server each have their own Neo4j port, systemd unit, VNC display, websockify port, tunnel, and persist directory.
- **No public IPv4 exposure.** The Hetzner firewall opens port 22 only; every operator-facing surface is fronted by the Cloudflare tunnel.

## Smoke checklist

Fresh-Hetzner smoke pass criteria:

1. Provision a CAX31 with Ubuntu 24.04 arm64 and an SSH key; SSH in as `root`, create `admin`, switch.
2. Install completes on the clean image, prints a loopback URL, and the systemd user-service is `active (running)`.
3. The noVNC page reached over `ssh -L 8080:localhost:<novnc-port>` displays the admin UI.
4. Cloudflare setup driven by the `cloudflare` plugin's `setup-tunnel` skill ends with `curl -I https://<hostname>.<your-zone>` returning `HTTP/2 200` from the operator's laptop.
5. Reboot the server; both `<hostname>.service` and `<hostname>-cloudflared.service` come back up; the public URL is reachable again without any manual action.
6. Install a second brand with a different `--hostname`; both brands' admin UIs are reachable on their own public hostnames and neither has touched the other's state.
7. Uninstall removes the systemd unit and the persist directory.

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