Remote Devbox — Hetzner + Tailscale + Blink
Always-on remote development server for Quarb/Michiu projects. Work from Mac, iPad, or iPhone without the Mac needing to be awake. All compute lives on the server; your devices are just terminals.
Server facts
| Item | Value |
|---|---|
| Provider | Hetzner Cloud |
| Plan | CX23 (2 vCPU, 4 GB RAM, 40 GB SSD) |
| Hostname | ubuntu-4gb-nbg1-1 |
| Public IP | 46.225.238.8 |
| Tailscale IP | 100.112.135.68 |
| Tailscale hostname | ubuntu-4gb-nbg1-1.tail….ts.net |
| OS | Ubuntu 24.04 |
| User | casper |
| SSH | Tailscale-only (public port 22 closed) |
Cost: ~€4.79/month (CX23 €3.99 + Hetzner backups €0.80). Tailscale personal = free.
Architecture
┌──────────────┐ ┌────────────────────┐
│ MacBook │ │ iPad / iPhone │
│ ssh/mosh │ │ Blink Shell + Mosh │
└──────┬───────┘ └──────┬──────────────┘
│ │
│ Tailscale (private mesh VPN)
└────────────┬───────────┘
│
┌─────────▼──────────────────┐
│ Hetzner CX23 │
│ ubuntu-4gb-nbg1-1 │
│ │
│ tmux sessions: │
│ michiu / quarb / │
│ ninja / aimi │
│ │
│ Node 22 + pnpm │
│ Claude Code 2.x │
│ Supabase CLI │
│ ~/projects/ │
│ ~/.secrets/ │
└────────────────────────────┘
│
GitHub (push/pull)
│
Vercel (deploy target)
All code runs on the server. Mac/iPad/iPhone only render the terminal. Closing Blink does not kill anything — tmux keeps sessions alive indefinitely.
Current status (confirmed 2026-05-16)
- Server provisioned and hardened
-
casperuser created, SSH key auth only - UFW: SSH + Mosh only on Tailscale interface; public port 22 closed
- fail2ban running
- Tailscale authenticated (server + Mac)
- Node 22.22.3, pnpm 11, Claude Code 2.1, Supabase CLI 2.98 installed
- 4 tmux sessions pre-created: michiu / quarb / ninja / aimi
-
michiu-marketing+michiu-websitecloned into~/projects/michiu/ -
.env.localsymlinked →~/.secrets/env.michiu(17 env vars, chmod 600) - Claude Code memory copied to correct server slug (
-home-casper-projects-michiu-michiu-marketing)
Daily workflow
Connect (any device)
mosh devbox # Blink: reconnects instantly, survives sleep + roaming
# or
ssh casper@ubuntu-4gb-nbg1-1.ts.net # if mosh not needed
Start a session
tmux attach -t michiu
cd ~/projects/michiu/michiu-marketing
git pull
claude
That's it. Claude Code loads CLAUDE.md + HANDOFF.md automatically. Pick up exactly where you left off.
End a session
Ctrl-a d # detach from tmux — everything keeps running
Close Blink. The server stays up. No need to stop anything.
tmux reference
The server has 4 pre-created sessions. Attach to whichever project you're working on.
tmux ls # list all sessions + windows
tmux attach -t michiu # attach to michiu session
tmux attach -t quarb # attach to quarb session
tmux attach -d # force-detach any stale clients first
Inside tmux (prefix is Ctrl-a):
| Action | Keys |
|---|---|
| Detach | Ctrl-a d |
| New window | Ctrl-a c |
| Switch window | Ctrl-a 0/1/2/3 |
| Rename window | Ctrl-a , |
| Split horizontal | Ctrl-a | |
| Split vertical | Ctrl-a - |
| Navigate panes | Ctrl-a + arrow keys |
| Kill pane | Ctrl-a x |
Blink Shell setup (iPad / iPhone)
-
App Store → Blink Shell (~€20 one-time)
-
Open Blink → type
config -
Keys → New Key → generate a device-specific key (keep separate from Mac key)
-
Add the public key to the server:
# on server: echo "ssh-ed25519 AAAA... ipad" >> ~/.ssh/authorized_keys -
Hosts → New Host:
- Alias:
devbox - HostName:
ubuntu-4gb-nbg1-1.tail….ts.net(your Tailscale hostname) - User:
casper - Key: the device key you just generated
- Alias:
-
Install Tailscale on the device and connect before using Blink.
Hardware keyboard tip: Remap Caps Lock → Control in iPad Settings → General → Keyboard → Hardware Keyboard. Makes tmux and vim usable.
Mosh vs SSH
Use mosh for daily work. Use ssh only for one-off quick commands from Mac.
| Mosh | SSH | |
|---|---|---|
| Survives iPad sleep | Yes | No |
| Survives WiFi → 4G switch | Yes | No |
| Works on spotty hotel WiFi | Yes | No |
| Reconnects automatically | Yes | No |
| Good for file transfers (scp) | No | Yes |
Mosh requires UDP 60000–61000 open in UFW (already configured).
Common commands — michiu-marketing
cd ~/projects/michiu/michiu-marketing
# Development
pnpm dev # Next.js dev server on :3000
pnpm build # typecheck + production build
pnpm typecheck # tsc --noEmit only
# Database
supabase db push # apply pending migrations to live Supabase
supabase db push --include-all # include all unapplied
supabase status # confirm linked project
# Testing
pnpm regress # review prompt regression suite
pnpm regress:dm # DM prompt regression suite
# Data
pnpm seed # seed mock data
pnpm import:google # import Google reviews
# Inngest (local event bus)
pnpm dlx inngest-cli@latest dev # local Inngest UI on :8288
# Claude Code
claude # start session (auto-loads CLAUDE.md + HANDOFF.md)
claude --continue # continue last session without prompt
Secrets management
All secrets live in ~/.secrets/env.michiu (chmod 600), symlinked to .env.local in each repo.
# Verify symlink
ls -la ~/projects/michiu/michiu-marketing/.env.local
# → .env.local -> /home/casper/.secrets/env.michiu
# Edit a secret
nano ~/.secrets/env.michiu # or vim
# Re-link if the symlink ever breaks
ln -sf ~/.secrets/env.michiu ~/projects/michiu/michiu-marketing/.env.local
When a secret changes (e.g. you rotate a key in Vercel):
- Update it in Vercel dashboard
- Update
~/.secrets/env.michiuon the server manually - Restart the dev server if it's running (
Ctrl-Cthenpnpm dev)
Never commit .env.local — it's gitignored and is a symlink anyway.
Current secret inventory
| Variable | Required | Notes |
|---|---|---|
NEXT_PUBLIC_SUPABASE_URL | Yes | Supabase project URL |
NEXT_PUBLIC_SUPABASE_ANON_KEY | Yes | Supabase anon key |
SUPABASE_SERVICE_ROLE_KEY | Yes | Server-only |
ADMIN_EMAIL_ALLOWLIST | Yes | csprkrn@gmail.com |
ANTHROPIC_API_KEY | Yes | console.anthropic.com |
ANTHROPIC_MODEL | Yes | claude-sonnet-4-6 |
REPLY_SIGNATURE_POOL | Yes | Comma-separated staff names |
FOOTBALL_DATA_API_KEY | Yes | football-data.org v4 |
NEXT_PUBLIC_APP_URL | Dev | http://localhost:3000 |
RESEND_API_KEY | Optional | Transactional email |
INNGEST_EVENT_KEY / INNGEST_SIGNING_KEY | Optional | Blank = local dev mode |
| Meta, LinkedIn, WhatsApp, Google Ads | Pending | Gated on platform approvals |
Full spec in .env.example.
Claude Code on the server
Claude Code is installed globally (npm i -g @anthropic-ai/claude-code). Memory is project-scoped.
Memory slot: /home/casper/.claude/projects/-home-casper-projects-michiu-michiu-marketing/memory/
This is separate from the Mac slot (-Users-CasperMacbook-...). Both are now in sync — the Mac memory was copied to the server slot on 2026-05-16.
If memory ever gets out of sync (e.g. after a long gap where Mac sessions happened):
# On Mac — sync current memory to server
bash scripts/devbox/sync-to-server.sh
# On server — copy to the correct slot
NEW="-home-casper-projects-michiu-michiu-marketing"
OLD="-Users-CasperMacbook-APP-STUDIO-MICHIU-Code-michiu-marketing"
cp ~/.claude/projects/$OLD/memory/* ~/.claude/projects/$NEW/memory/
CLAUDE.md + lane files are in the repo and load automatically — no special setup needed.
Auth: Claude Code auth tokens live on the server. If they expire:
claude logout && claude login
Obsidian vault sync
The MICHIU vault lives on iCloud on the Mac. To read it on the server, mirror it to a private GitHub repo.
One-time setup (on Mac)
cd ~/Library/Mobile\ Documents/iCloud~md~obsidian/Documents/MICHIU
git init
cat > .gitignore <<'EOF'
.obsidian/workspace.json
.obsidian/cache
.trash/
EOF
git add . && git commit -m "init: MICHIU vault"
git remote add origin git@github.com:quarbstudio/michiu-brand-vault.git
git push -u origin main
Create the repo first: gh repo create quarbstudio/michiu-brand-vault --private
Clone on server
mkdir -p ~/vaults
git clone git@github.com:quarbstudio/michiu-brand-vault.git ~/vaults/michiu
Sync workflow
Mac → server (after editing in Obsidian):
cd ~/Library/Mobile\ Documents/iCloud~md~obsidian/Documents/MICHIU
git add . && git commit -m "vault: $(date +%F)" && git push
Server (before a session that needs vault context):
cd ~/vaults/michiu && git pull
Tip: Install the Obsidian Git plugin in the vault to auto-commit + push every 10 minutes. Then git pull on the server is the only manual step.
Switching between ventures
The server has 4 tmux sessions. Use them to keep project contexts separate.
Ctrl-a d # detach current session
tmux attach -t quarb # switch to Quarb projects
tmux attach -t ninja # switch to Ninja project
tmux attach -t aimi # switch to Aimi project
Each session can have multiple windows (Ctrl-a c) for different repos or tasks within the same venture.
Syncing secrets from Mac
When onboarding a new project or after rotating secrets, use the sync script:
# On Mac, from michiu-marketing repo root:
bash scripts/devbox/sync-to-server.sh
This copies ~/.secrets/ and Claude memory from Mac to server. Run it any time secrets change on the Mac side.
Maintenance
Weekly
sudo apt update && sudo apt upgrade -y
npm update -g @anthropic-ai/claude-code
pnpm add -g pnpm@latest
Monthly
sudo fail2ban-client status sshd # check blocked IPs
df -h # disk usage (CX23 = 40 GB)
free -h # memory pressure
Upgrade path: If you consistently see memory pressure from multiple Claude Code sessions, upgrade to CX33 (8 GB RAM, ~€8/mo) in the Hetzner Cloud console — no data loss, live resize.
Backups
Enable in Hetzner Console → server → Backups (~€0.80/mo). Code is in GitHub. What isn't: ~/.secrets/, Claude auth tokens, tmux state. The backup covers all of these.
Troubleshooting
| Problem | Fix |
|---|---|
| "Connection refused" after firewall change | Use Hetzner web console (console.hetzner.cloud → server → Console) to fix UFW without SSH |
| Mosh hangs on connect | sudo ufw allow 60000:61000/udp && sudo ufw reload |
| tmux session won't attach | tmux attach -d -t michiu (force-detach stale client) |
| Claude Code auth expired | claude logout && claude login |
.env.local missing | ln -sf ~/.secrets/env.michiu ~/projects/michiu/michiu-marketing/.env.local |
supabase not linked | supabase login && supabase link --project-ref <ref> (ref in Supabase dashboard → Settings → General) |
pnpm install fails ENOSPC | pnpm store prune — or upgrade to CX33 if disk is full |
nvm not found in new shell | Add to ~/.bashrc: export NVM_DIR="$HOME/.nvm" && [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" |
| Server unreachable (Tailscale down) | SSH via public IP temporarily: ssh casper@46.225.238.8 — then fix Tailscale |
| Tailscale not connected on device | Open Tailscale app, toggle off/on — or sudo tailscale up on server |
git push fails (auth) | ssh -T git@github.com — if it fails, re-add the server SSH key to GitHub |
Security checklist
- Root SSH login disabled (
PermitRootLogin no) - Password auth disabled (
PasswordAuthentication no) - UFW enabled: SSH + Mosh UDP 60000–61000 on Tailscale interface only
- Public port 22 closed to internet
- fail2ban running
- Unattended upgrades enabled
- Tailscale authenticated on server + Mac
- Tailscale installed on iPad (required before Blink works)
- Tailscale installed on iPhone (required before Blink works)
- Separate SSH keys per device: Mac, iPad, iPhone
- GitHub SSH key is devbox-specific (not reused from Mac)
-
~/.secrets/ischmod 700; each filechmod 600 -
.env.localis a symlink (not committed — confirmed in.gitignore) - Hetzner backups enabled
Quick reference cheatsheet
# ── Connect ──────────────────────────────────────────
mosh devbox # iPad/iPhone via Blink (recommended)
ssh casper@ubuntu-4gb-nbg1-1.ts.net # Mac via Tailscale hostname
ssh casper@46.225.238.8 # Mac via public IP (fallback only)
# ── tmux ─────────────────────────────────────────────
tmux ls # list sessions
tmux attach -t michiu # attach to michiu
tmux attach -d # force-detach stale clients
Ctrl-a d # detach
Ctrl-a c / Ctrl-a 0/1/2/3 # new window / switch window
Ctrl-a | / Ctrl-a - # split horizontal / vertical
Ctrl-a , # rename window
# ── Start a session ──────────────────────────────────
tmux attach -t michiu
cd ~/projects/michiu/michiu-marketing
git pull
claude
# ── michiu-marketing ─────────────────────────────────
pnpm dev # dev server :3000
pnpm build # typecheck + build
pnpm typecheck # typecheck only
pnpm regress # review prompt regression
supabase db push --include-all # apply pending migrations
claude # Claude Code session
# ── Tailscale ────────────────────────────────────────
tailscale status # all devices + IPs
tailscale ip -4 # this server's tailnet IP
sudo tailscale up # reconnect if disconnected
# ── Server health ────────────────────────────────────
htop # processes + memory
df -h # disk
free -h # RAM
sudo systemctl status fail2ban # security daemon
# ── Vault sync ───────────────────────────────────────
cd ~/vaults/michiu && git pull # pull latest vault from GitHub