# Pi Command Server: build guide

From the OpsCode resources store: https://opscode.ai/resources/pi-command-server/
This is the exact sequence we followed for our own always-on AI dev server. Parts list is in `pi-parts-checklist.md`. Tablet and phone driving is in `termux-tmux-cheatsheet.md`.

What you end up with: a Raspberry Pi 5 that runs Claude Code sessions 24/7, reachable from your tablet, phone and laptop over a private encrypted network, booting from fast NVMe storage, that heals itself after power cuts and backs itself up nightly.

---

## Part 1: Assembly (under an hour, no soldering)

1. **Active cooler.** Peel the film off the thermal pads. Clip the cooler onto the Pi's main chip (two push pins). Plug its cable into the small fan header on the board edge.
2. **NVMe base.** Screw the SSD into the base's M.2 slot at a shallow angle, then flat. Connect the short ribbon cable from the base to the Pi's PCIe port (lift the black latch, slide the ribbon in, close the latch). The Pi mounts on standoffs above the base.
3. **RTC battery.** The plug clicks into the small white connector next to the USB-C power port. Stick the coin cell to the inside of the case with its adhesive pad. This keeps the clock right through power cuts, which keeps HTTPS certificates and scheduled jobs from breaking.
4. **Case.** Drop the stack into the aluminium case and screw it shut. The case is also a heatsink.
5. **First boot.** Insert the pre-loaded SD card, plug in ethernet (recommended) and the 27W supply. Give it 30 seconds. No monitor needed, ever.

## Part 2: First login and lockdown

From any computer on the same network:

```bash
ping raspberrypi.local          # find it
ssh pi@raspberrypi.local        # default password: raspberry
passwd                          # change it immediately
sudo apt update && sudo apt upgrade -y
```

Create your own user if you want a name that isn't `pi` (optional but tidy):

```bash
sudo adduser yourname
sudo usermod -aG sudo yourname
```

## Part 3: The core stack

```bash
# Node.js 20 LTS
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt install -y nodejs

# Claude Code
npm install -g @anthropic-ai/claude-code

# Essentials
sudo apt install -y tmux git git-lfs gh
git lfs install

# nano as the editor Claude Code opens for long prompts
echo 'export EDITOR=nano' >> ~/.bashrc
source ~/.bashrc

# Verify
node --version && claude --version && gh --version
```

Then Git identity and GitHub:

```bash
git config --global user.name "Your Name"
git config --global user.email "you@example.com"
gh auth login        # GitHub.com → HTTPS → login with browser
```

## Part 4: The private network (Tailscale)

Tailscale is a WireGuard mesh. No port forwarding, no exposed services, works from any internet connection.

```bash
curl -fsSL https://tailscale.com/install.sh | sh
sudo tailscale up    # approve the device in the browser
tailscale ip -4      # note the Pi's 100.x.y.z address
```

Install the Tailscale app on your tablet, phone and laptop, signed into the same account. Every device can now reach the Pi from anywhere. Turn on MagicDNS in the Tailscale admin console and the Pi gets a friendly name too.

## Part 5: Boot from NVMe

The SD card got you running. The SSD is 10x faster and far more durable.

```bash
# 1. Latest bootloader, NVMe-first boot order
sudo rpi-eeprom-update -a
sudo rpi-eeprom-config --edit     # set: BOOT_ORDER=0xf416

# 2. Confirm the drive is visible
lsblk -o NAME,MODEL,SIZE          # expect nvme0n1 with your SSD's model
```

**Cloning warning, learned the hard way:** the popular `rpi-clone` script has a bug with NVMe targets. It builds partition names as `nvme0n11` instead of `nvme0n1p1` and aborts partway. Do it manually instead:

```bash
# 3. Partition the NVMe like the SD (one FAT32 boot, one ext4 root),
#    format, then rsync both filesystems across:
sudo mkfs.vfat -F 32 /dev/nvme0n1p1
sudo mkfs.ext4 /dev/nvme0n1p2
sudo mount /dev/nvme0n1p2 /mnt
sudo mkdir -p /mnt/boot/firmware
sudo mount /dev/nvme0n1p1 /mnt/boot/firmware
sudo rsync -axHAX --info=progress2 / /mnt/
sudo rsync -axHAX /boot/firmware/ /mnt/boot/firmware/

# 4. Point the clone at itself: update PARTUUIDs in
#    /mnt/etc/fstab and /mnt/boot/firmware/cmdline.txt
#    (get the new ones with: sudo blkid)

# 5. Shut down, pull the SD card, power on. Then check:
df -h /                           # should show /dev/nvme0n1p2
```

Keep the SD card in a drawer, labelled with the date. If NVMe boot ever fails, put it back in and you're running again in a minute.

## Part 6: Survive reboots

The goal: a power cut is a non-event.

**tmux sessions that restore themselves:**

```bash
git clone https://github.com/tmux-plugins/tpm ~/.tmux/plugins/tpm
```

Add to `~/.tmux.conf`:

```
set -g mouse on
set -g @plugin 'tmux-plugins/tpm'
set -g @plugin 'tmux-plugins/tmux-resurrect'
set -g @plugin 'tmux-plugins/tmux-continuum'
set -g @continuum-restore 'on'
set -g @continuum-save-interval '15'
run '~/.tmux/plugins/tpm/tpm'
```

Then inside tmux press `Ctrl+B` then `I` to install the plugins. Sessions now auto-save every 15 minutes and restore after a reboot.

**systemd for anything long-running** (one unit per service):

```ini
# /etc/systemd/system/myservice.service
[Unit]
Description=My always-on thing
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=yourname
WorkingDirectory=/home/yourname/myapp
ExecStart=/usr/bin/node server.js
Restart=always
RestartSec=10

[Install]
WantedBy=multi-user.target
```

```bash
sudo systemctl enable --now myservice.service
```

**Optional: a boot alert.** A one-shot systemd unit that pings you (Telegram, Slack, email) whenever the Pi finishes booting. Every unexpected reboot becomes a notification instead of a mystery.

## Part 7: Nightly backups

Code lives in git already. This covers everything else: tokens, configs, databases.

```bash
sudo apt install -y restic rclone
rclone config      # add your cloud storage (Google Drive, S3, B2...)

# strong passphrase, stored OUTSIDE the Pi too (password manager)
echo "YOUR_PASSPHRASE" | sudo tee /root/.restic-pass
sudo chmod 600 /root/.restic-pass

sudo RESTIC_PASSWORD_FILE=/root/.restic-pass restic \
  -r rclone:yourremote:pi-backups init
```

Back up `/home`, `/etc` and service state nightly via a systemd timer (2am works). Exclude caches, `node_modules` and virtualenvs. restic deduplicates, so after the first run each night only uploads 50 to 200 MB. Keep 30 daily and 12 monthly snapshots:

```bash
restic forget --keep-daily 30 --keep-monthly 12 --prune
```

**Untested backups are not backups.** Once a quarter, restore a snapshot to `/tmp` and check a few critical files are really there.

## Part 8: A full IDE in the browser (optional)

code-server is VS Code served from the Pi:

```bash
curl -fsSL https://code-server.dev/install.sh | sh
sudo systemctl enable --now code-server@$USER
```

Serve it over HTTPS inside your tailnet only:

```bash
sudo tailscale serve --bg --https=443 http://127.0.0.1:8080
```

Your tablet browser now opens full VS Code at `https://<your-pi>.<your-tailnet>.ts.net`. Nothing is exposed to the public internet.

## Troubleshooting quick hits

| Problem | Fix |
|---|---|
| Can't find the Pi | `ping raspberrypi.local`, or check the router's device list |
| SSH drops from tablet | run `termux-wake-lock`, set Termux battery to Unrestricted |
| Can't reach a served page | the server must bind `0.0.0.0`, not `localhost` |
| Pi missing from tailnet | `sudo tailscale up` on the Pi |
| Wrong clock after power cut | check the RTC battery is plugged in; `sudo hwclock --show` |
| Garbled terminal after crash | type `reset`, or reconnect SSH |

---

Built and documented by OpsCode, Operations as Code. https://opscode.ai
