Building the Underworld

A complete guide to building a homelab on Oracle Cloud's free tier, managed entirely from your phone, with an AI agent doing the heavy lifting.

By Kevin Paul · March 2026 · Built on hades-vnic

Chapter 1

The Oracle Problem

For two years, my web development workflow was the same miserable loop: edit files locally, upload them via cPanel's file manager, restart the Node.js app through Phusion Passenger, check the browser, find a bug, and do it all again. The server was VentraIP shared hosting at s03ad.syd2.hostingplatform.net.au — $11/month for a cPanel box in Sydney with Passenger support and LiteSpeed lsnode.

It worked. Barely. The problem wasn't the hosting — it was the feedback loop. Every change required a manual upload. There was no SSH, no git deploy, no way to iterate quickly. I started writing .md files as documentation and context-keeping, because I couldn't maintain the mental model across sessions. The project would get too complex to remember, the README would go stale, and I'd come back to code I didn't recognize.

Then I started using Claude. First through the web UI at claude.ai — good for thinking through problems, bad for actually editing code. You'd paste code in, get suggestions back, manually apply them, re-paste. Another loop. Another upload cycle.

Claude Code changed everything.

Claude Code is a CLI tool that runs in your terminal. It has direct access to your filesystem — it can read files, edit them, run commands, check git status, build Docker images, and test things. Instead of copy-pasting between a chat window and an editor, you just talk to it, and it works on your actual codebase.

The difference is like telling someone how to renovate your kitchen by reading instructions over the phone, versus handing them the tools and standing next to them. Claude Code has the tools. It can cat your config files, grep for that error, docker logs the container, and fix the issue — all in one conversation.

But Claude Code needs a server to run on. Not a laptop (mine is old). Not a cPanel box (no SSH). Something I could SSH into, leave running, and access from anywhere — including my phone.

I needed a homelab. And Oracle was about to give me one for free.

Chapter 2

Descent: Getting the Free Server

Oracle Cloud Infrastructure (OCI) has an Always Free tier that includes an ARM-based VM with up to 4 OCPUs and 24GB of RAM. That's not a typo — 24 gigabytes. For free. Forever.

The shape is called VM.Standard.A1.Flex (Ampere). It runs on ARM64 processors, which means you need aarch64 builds for everything (most things have these now, Docker included). For the price of zero dollars, this is absurdly powerful.

Creating your OCI account

  1. Go to cloud.oracle.com/free
  2. Sign up as Individual (not Company) — it's a personal homelab
  3. Choose your Home Region: Australia East (Sydney)
Your home region cannot be changed

This is permanent. Pick the region closest to you. Sydney for Australia. If you pick the wrong one, you'd need to create a new tenancy. My tenancy is named kpaul.

You'll need a credit card for verification, but the Always Free tier resources genuinely don't cost anything. The 30-day trial gives you $300 in credits for paid resources — we'll use this strategically in the next chapter.

What you get for free

One machine with all 4 OCPUs and 24GB is the sweet spot. I named mine hades.

Chapter 3

The Capacity Problem and the Workaround

Here's the first gotcha that will cost you hours if you don't know about it.

When you try to create a VM.Standard.A1.Flex instance, there is a very good chance you'll get this:

Out of capacity for shape VM.Standard.A1.Flex in availability domain AD-1

This is not your fault. Oracle's free ARM capacity is genuinely oversubscribed. People write scripts to retry every 60 seconds for days. There are GitHub repos dedicated to this. It's a known problem.

But there's a workaround the community discovered, and it only works during your 30-day trial period.

The A2-to-A1 conversion trick

The VM.Standard.A2.Flex shape (also ARM, newer Ampere) is typically available during the trial because it's a paid shape that uses your trial credits. The trick is:

  1. Create an A2.Flex instance with the specs you want (4 OCPU, 24GB)
  2. Stop the instance
  3. Use the OCI CLI (via Cloud Shell) to convert it to A1.Flex
  4. Start it — you now have an Always Free A1.Flex instance
# Open Cloud Shell from the OCI Console (top-right terminal icon)
# Stop the instance first, then run:

oci compute instance update --instance-id YOUR_INSTANCE_OCID \
  --shape VM.Standard.A1.Flex \
  --shape-config '{"ocpus": 4, "memory_in_gbs": 24}'
This only works during the trial

After the 30-day trial ends, you can't create or modify paid shapes. Do the conversion while you still have trial credits. Once it's A1.Flex, it stays A1.Flex — free forever.

Find your instance OCID in the OCI Console: Compute → Instances → your instance → Instance Details. It starts with ocid1.instance.oc1...

After the conversion, start the instance. You now have a free ARM server in Sydney with more RAM than most people's laptops.

Chapter 4

Building the Underworld: Network Setup

OCI's networking is powerful but has several gotchas that will waste your time if you don't know about them. The mythology writes itself here: your network is the river system of the underworld.

Create the VCN (Styx)

A Virtual Cloud Network. Networking → Virtual Cloud Networks → Create VCN.

Do NOT use "Create VCN with Internet Connectivity" wizard

The inline subnet creation wizard doesn't properly mark subnets as public. The "public subnet" toggle stays greyed out even though it says public. Create the VCN first, then create the subnet separately. This cost me genuine confusion.

Create the Subnet (Acheron)

Go into your VCN → Subnets → Create Subnet (separate from VCN creation).

Create the Internet Gateway (Charon)

Still inside the VCN: Internet Gateways → Create.

Route Table

Route Tables → Default Route Table → Add Route Rules

Security List (Ingress Rules)

Security Lists → Default Security List → Add Ingress Rules

Add stateful TCP ingress from 0.0.0.0/0:

Two firewalls, not one

OCI has its own security layer (Security Lists) that exists outside the VM. Your VM also runs UFW. Both must allow the traffic. If something doesn't work, check both layers. This is the most common "why can't I connect" issue on OCI.

We'll add more rules later: UDP 41641 for Tailscale's WireGuard, UDP 3478 for Headscale's STUN/DERP, and TCP 143/465/587/993 for mail.

Chapter 5

Naming Hades

With the network ready, create the instance.

Compute → Instances → Create Instance

The SSH key

OCI generates a key pair or lets you upload your own. If you let it generate one:

Download the private key immediately

OCI shows the private key once. If you close the dialog or navigate away, it's gone. There is no "download again" button. Save both the private key (.key) and public key. If you lose the private key, you lose SSH access and must recreate the instance.

I named the key id_ed25519 and stored it at ~/.ssh/id_ed25519 on hades after first login. The comment on the key is hades-vnic.

On naming things well: every resource in this guide has a name from Greek mythology. The VCN is Styx, the subnet is Acheron, the gateway is Charon, the server is Hades. This isn't just for fun — when you have a dozen resources, descriptive names save you from clicking into each one to remember what it does. Name things well from day one.

Chapter 6

First Contact: SSH and the Stack

This entire homelab was built from an iPhone. Not a laptop. An iPhone running Termius (SSH client for iOS) over a coffee shop WiFi connection. That's the constraint that shaped everything.

Connecting with Termius

  1. Import your SSH private key into Termius (Settings → Keychain)
  2. Create a new host: ubuntu@YOUR_PUBLIC_IP
  3. Set the key as the authentication method
The username is ubuntu, not root

OCI Ubuntu images use ubuntu as the default user, not root. Root login via SSH is disabled. Use sudo for privileged commands.

Initial setup

# Update everything
sudo apt update && sudo apt upgrade -y

# Install basics
sudo apt install -y git curl wget htop ufw fail2ban unzip

# Install Node.js (v22 LTS)
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
sudo apt install -y nodejs

Installing Claude Code

# Install globally (don't use sudo npm — use the Node install method)
npm install -g @anthropic-ai/claude-code
Don't use sudo npm install -g

Using sudo npm install -g can mess up file permissions for your user. If npm doesn't have write access to the global directory, fix the prefix: npm config set prefix ~/.npm-global and add ~/.npm-global/bin to your PATH.

Authenticating Claude Code

Run claude and it will prompt you to authenticate via claude.ai. It gives you a URL to open in a browser.

Termius URL copy issue

Termius on iOS sometimes truncates long URLs when you try to copy them from terminal output. The OAuth callback URL from Claude Code is very long. If it's cut off, try selecting the URL carefully, or use a proper terminal that handles long lines. The Helm dashboard's web terminal (Chapter 8) handles this better.

Once authenticated, Claude Code stores the session token and you're ready. This is the tool that will build everything from here.

Chapter 7

Tailscale and the Closed Gate

With SSH working over the public IP, the first priority is locking that down. Port 22 open to the internet is an invitation for brute-force bots (fail2ban helps, but why leave the door open at all?).

The plan: install Tailscale for a private mesh network, then close port 22 to the public internet. All SSH access goes through Tailscale's encrypted WireGuard tunnel.

Installing Tailscale

curl -fsSL https://tailscale.com/install.sh | sh

# Use the auth key method (no browser needed on the server)
# Generate an auth key at https://login.tailscale.com/admin/settings/keys
sudo tailscale up --authkey tskey-auth-XXXXX

Now hades has a Tailscale IP (100.107.149.72). Install Tailscale on your phone too, and you can SSH to hades from anywhere — coffee shop, train, bed — through the encrypted tunnel.

Adding OCI security rule for Tailscale

Tailscale uses WireGuard on UDP 41641. Add it to your OCI Security List:

Locking down SSH

# Enable UFW
sudo ufw default deny incoming
sudo ufw default allow outgoing

# Allow Tailscale interface (all traffic over the tunnel)
sudo ufw allow in on tailscale0

# Allow HTTP/HTTPS for web services
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp

# DO NOT allow port 22 from public — Tailscale only
sudo ufw enable
Test Tailscale SSH before locking down port 22

Before enabling UFW (which blocks public port 22), confirm you can SSH via the Tailscale IP. If Tailscale isn't working, you'll lock yourself out. Test with: ssh [email protected] from your Tailscale-connected device.

The Meraki problem and Headscale

This worked perfectly until I got to the office. My workplace runs Cisco Meraki for network management, and Meraki blocks Tailscale's coordination servers. The VPN tunnel couldn't establish because my phone couldn't reach controlplane.tailscale.com.

The fix: Headscale — an open-source, self-hosted replacement for Tailscale's coordination server. If you run your own coordination server on a domain you control, Meraki can't block it (it doesn't know what headscale.kevinpaul.au is).

# Headscale runs as a Docker container on hades itself
# Config at /opt/headscale/config/config.yaml
# Compose at /opt/headscale/docker-compose.yaml

# Caddy reverse proxies headscale.kevinpaul.au → headscale:8080
# DNS: headscale.kevinpaul.au → 152.69.172.49 (DNS only, no Cloudflare proxy)

Headscale v0.28 runs with an embedded DERP relay server (region 900, "Sydney (Hades)") and STUN on UDP 3478. This means your devices can find each other even behind NAT, without relying on Tailscale's public infrastructure.

To register a device with your Headscale server:

# On the device:
tailscale up --login-server https://headscale.kevinpaul.au --authkey KEY

# Generate a key:
docker exec headscale headscale preauthkeys create --user 1 --expiration 1h

# Register a node manually:
docker exec headscale headscale nodes register --key NODE_KEY --user paul
Headscale CLI gotcha

Headscale v0.28 uses numeric user IDs for preauthkeys commands (--user 1) but string names for nodes register (--user paul). Inconsistent, but that's the API.

Current nodes on the network: iphone-14-pro (100.64.0.1), hades-vnic (100.64.0.2), and twt-kev-desktop (just registered).

Chapter 8

Claude Code's Home: tmux and Remote Control

Claude Code runs as a long-lived terminal process. If your SSH session drops (phone locks, network switches, you walk into a lift), the process dies and you lose context. The fix: tmux.

The tmux setup

# start-claude.sh — saved at /home/ubuntu/start-claude.sh
#!/bin/bash
tmux new-session -d -s claude 'claude --dangerously-skip-permissions' 2>/dev/null || tmux attach -t claude

This creates a tmux session named claude that survives disconnects. When you SSH back in, tmux attach -t claude picks up exactly where you left off — Claude Code is still running, full context intact.

I run two concurrent sessions: claude and claude-2. Two agents, two tasks, same server.

Remote Control

Claude Code has a feature called Remote Control (/remote-control or /rc). This lets you control a running Claude Code instance from the Claude iOS app or claude.ai in a browser. You get a pairing code, enter it in the app, and your messages route to the terminal session on hades.

Here's the important nuance: Remote Control still prompts for permission on tool calls. If Claude Code wants to edit a file or run a command, it asks for permission — but the permission prompt appears in the terminal (tmux), not in the Remote Control interface. So you can't approve it from your phone.

Best practice for Remote Control

Use tmux (direct terminal) to initiate and drive tasks — that's where permission prompts appear and can be approved. Use Remote Control for inspection, chatting, and light queries — asking about code, reviewing state, or having Claude explain something. Don't try to run major build tasks through Remote Control.

For tasks that need to run without prompting, set bypassPermissions in settings:

# ~/.claude/settings.json
{
  "permissions": {
    "allow": [],
    "deny": [],
    "bypassPermissions": true
  }
}
bypassPermissions on a production server

This skips all permission checks — Claude Code can delete files, push to git, restart containers, anything. On a personal homelab that's acceptable. On a shared server, don't. I also have a custom safety hook (~/.claude/hooks/hades-guard.sh) that blocks genuinely dangerous commands like rm -rf and git push --force.

The Helm dashboard terminal

There's a better option emerging: the Helm dashboard at helm.kevinpaul.au. This is a web-based admin panel for hades with a built-in terminal (xterm.js + node-pty). It runs as a systemd service on the host (not Docker) because it needs access to /proc, tmux, the Docker socket, and the filesystem.

The Helm terminal is showing better promise than Termius for mobile use — it handles long lines properly, doesn't have the URL truncation issue, and integrates with the rest of the dashboard (container status, system stats, file explorer). For managing Claude Code sessions from a phone, this is the direction things are heading.

Chapter 9

The Homelab Stack

With SSH, Tailscale, and Claude Code running, it's time to build the infrastructure that everything else sits on.

Docker

# Install Docker CE
curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker ubuntu

# Create the shared network
docker network create homelab

Every container goes on the homelab network. This lets containers reach each other by name — caddy can proxy to agora:3000, gatus can health-check headscale:8080, etc.

Caddy (The Gatekeeper)

Caddy is a reverse proxy that automatically handles SSL certificates. No certbot, no cron renewal, no nginx config debugging. You write a Caddyfile and it just works.

# /opt/caddy/Caddyfile — this is all you need for a new service
wiki.kevinpaul.au {
    reverse_proxy wikigame:3000
}

# Static files
dice.kevinpaul.au {
    handle /ws* {
        reverse_proxy dice-signaling:8080
    }
    handle {
        root * /srv/dice
        try_files {path} {path}/ /index.html
        file_server
    }
}

Caddy runs in a Docker container on the homelab network with ports 80 and 443 exposed. It auto-provisions Let's Encrypt certificates for every domain in the Caddyfile.

Cloudflare

DNS for kevinpaul.au and orthodoxy.au is managed through Cloudflare. Most domains are orange-clouded (proxied through Cloudflare's CDN), which gives you DDoS protection and hides the origin IP. Some — like headscale.kevinpaul.au and mail.kevinpaul.au — must be DNS-only (gray cloud) because their protocols don't work through Cloudflare's proxy.

Portainer

Web UI for managing Docker containers. Accessible at portainer.kevinpaul.au through Caddy. Useful for quick checks but Claude Code has replaced most of what I used Portainer for.

Gatus (The Watchtower)

Config-driven uptime monitoring. Checks health endpoints every 60–300 seconds and shows a dashboard at gatus.kevinpaul.au. Monitors every service — frontend, backend, internal health endpoints, mail ports, Headscale API.

The fractal MD structure

This is the secret weapon for working with Claude Code across sessions. The documentation is layered:

When Claude Code starts a new conversation, it reads CLAUDE.md and its memory files. It knows what's running, where configs live, what secrets exist, and what not to break. The MD files are the persistent brain — the conversation is just the working memory.

Chapter 10

What Lives on Hades

Every service here was built by Claude Code in a terminal session, managed from an iPhone. All vanilla JavaScript, no frameworks, no build steps.

WikiGame (wiki.kevinpaul.au)

Wikipedia shortest path finder. Enter two Wikipedia articles, it finds the fewest clicks between them using bidirectional BFS — searching forward from the start and backward from the target, meeting in the middle. Uses the Wikipedia Action API directly, rate-limited to 10 searches/minute per IP. A friend Gareth's idea.

Haggle (haggle.kevinpaul.au)

AI-powered translation tool for street market haggling. Translates English into Tagalog or Japanese with three tone variants: Formal, Casual, and Informal/Pushy. Uses Claude Haiku (the fast, cheap model) for translations. Also Gareth's idea, born from a trip to the markets.

Gorgon (gorgon.orthodoxy.au)

Byzantine chant notation writer. A web tool for composing Byzantine music notation — the kind used in Orthodox churches. Uses the Neanes font (GPL-3.0) with 375 SBMuFL-mapped glyphs across 13 palette categories. No equivalent web tool exists; current options (Neanes, Kassia) are desktop-only. This is for the choirs in my archdiocese.

Agora (agora.orthodoxy.au)

Orthodox Event Finder for Sydney. An adapter-based aggregator that pulls events from different parishes — each parish publishes differently (WhatsApp posters, Google Calendar, manual entry), so each gets a custom adapter. Uses Claude Haiku's vision to parse WhatsApp poster images into structured event data. SQLite for persistence, Leaflet for maps.

Dice (dice.kevinpaul.au)

P2P signaling server for a WebRTC-based dice game. This is the project that started it all — a way for ~10 friends to play tabletop RPGs online. The signaling server is a stateless WebSocket switchboard; all game data flows peer-to-peer. The server runs in Docker (dice-signaling container); the frontend is static files served directly by Caddy from the git repo via bind mount. Auto-deployed via GitHub webhook.

orthodoxy.au

A minimal launcher page — an iOS-style picker wheel for Australian Orthodox jurisdictions. Static HTML, no server, served directly by Caddy.

The mail server (mail.kevinpaul.au)

This one has a story. Self-hosted email on OCI is a split-brain architecture born from constraint:

The solution is a split between hades and the VentraIP cPanel box:

VentraIP cPanel (s03ad.syd2.hostingplatform.net.au) ├── MX record points here (receives inbound mail on port 25) ├── Outbound SMTP relay (port 587, proper PTR records) └── Mailbox: [email protected] Hades (docker-mailserver) ├── Fetchmail pulls from VentraIP via IMAPS every 120s ├── Dovecot stores mail locally (IMAP on port 993) ├── Postfix relays outbound through VentraIP:587 └── DKIM signing, Rspamd spam filtering iOS Mail connects to → hades:993 (IMAP) + hades:587 (SMTP) Inbound path: Internet → VentraIP:25 → fetchmail → hades Dovecot Outbound path: hades Postfix → VentraIP:587 → Internet

It works. Your phone talks to hades for both sending and receiving. VentraIP handles the port 25 and PTR issues invisibly. The setup was painful — docker-mailserver overwrites manual config on restart, and we burned three restart-diagnose cycles on a fetchmail uid mismatch before discovering the user-patches.sh escape hatch.

Chapter 11

The Brains Trust: Minthe

This chapter documents a planned architecture. Minthe is not yet deployed on hades.

The vision: a single natural language interface that routes to multiple AI agents, each with a specific domain. You talk to Minthe via Telegram, and she figures out which agent handles your request. The routing is invisible to the user — you just ask, and the right thing happens.

The agents

Memory layers

The design philosophy

One interface, many brains. The user shouldn't need to know which agent is handling their request. "What's the status of the mail server?" routes to Scarecrow. "Remind me about the haggle translation feature" routes to Lucienne. "Find me a Byzantine chant PDF" routes to Crow. You just ask.

The Telegram interface is key — it's the one app that works everywhere, has no message limits, supports rich formatting, and doesn't require a laptop.

Chapter 12

The Workflow Now

Three paths to the underworld, each for a different job:

Path 1: Terminal (the main road)

iPhone → Termius (or Helm dashboard terminal) → Tailscale/Headscale tunnel → tmux session on hades → Claude Code (full permissions, persistent context)

This is where real work happens. Claude Code has full filesystem access, can run Docker commands, edit configs, push to git. Two tmux sessions running concurrently. The Helm dashboard at helm.kevinpaul.au is emerging as a better terminal than Termius — proper xterm.js rendering, no URL truncation, integrated with system monitoring.

Path 2: Remote Control (the side door)

iPhone → Claude iOS app (or claude.ai) → Remote Control pairing → Routes to Claude Code on hades

Good for inspection, chat, and light queries. "What containers are running?" "Explain this error." "What did we change in the last commit?" Not ideal for tasks that need tool permissions — those prompts appear in the terminal, not the Remote Control UI.

Path 3: Cloud (the express lane)

iPhone / Browser → claude.ai/code (Anthropic cloud) → Stateless, no server access → Frontend work, planning, writing

For things that don't need to touch hades. Frontend code, design decisions, writing documentation, planning architectures. Quick and doesn't consume hades resources.

The clean separation

Cloud for stateless work. Hades for everything that needs to persist — Docker containers, databases, git repos, config files, monitoring, the homelab itself.

The whole point of this setup is that the server is always on, always reachable, always ready. You can pick up your phone at 2am, tmux attach to a session that's been running for days, and continue exactly where you left off. The AI agent has full context. The documentation is self-maintaining. The monitoring tells you if something broke while you weren't looking.

This is what a homelab looks like in 2026: a free ARM server in Sydney, a mesh VPN through a self-hosted coordination server, an AI agent in a persistent terminal session, and a phone as the only client you need.


Built entirely on hades-vnic · OCI ARM A1.Flex · Ubuntu 24.04
Written by Claude Code in a tmux session, from an iPhone