How I'm Built

A single virtual server hosts this site and every project alongside it, behind one web server, maintained conversationally by an AI agent running on the same box.

The idea

Most personal projects die in the gap between "it works on my laptop" and "it is on the internet." The goal here was to close that gap permanently: one server, one address, and a repeatable path from idea to live URL that does not involve remembering how any of it works.

The result is an additive host. New projects are added; nothing is torn down to make room. Each one is reachable under the same address at its own path.

This page is a live example of that gap being closed in real time. I'm not SSH'd in editing config by hand — I'm texting an AI agent (OpenClaw, running Claude) from my phone, in plain English, and it's reading the codebase, editing the files, rebuilding, and redeploying this exact site while you're reading this sentence. That's the actual hook of this whole project: the server doesn't just host the work, it maintains itself conversationally. No terminal, no dashboard — just a chat thread that turns into a shipped change on a live URL, every time.

From a text message to a live URL

The workflow that built this exact paragraph, end to end: I text a plain-English request from my phone. The agent reads the relevant repo, makes the edit, rebuilds it, deploys it, backs it up, and replies with the live link — no step in between touches a terminal on my end.

User (Ryan), on Telegram
plain English — e.g. “renew all the questions in still curious”
OpenClaw gateway
Node.js, loopback only, allowlisted sender — reads the repo, edits files, runs shell commands
/srv/projects/<name>
git repo, source of truth
Build step
npm run build — or nothing, for static-file projects
/srv/www/<name> or 127.0.0.1:70xx
build output, or a systemd service for dynamic apps
Caddy
routes this path already, or a new block is added and reloaded
https://ryantpham.com/<name>/
live, HTTPS, verified with a real request
git commit + push
GitHub remote — backup, survives the VPS dying
One reply on Telegram
what changed, plus the live URL

"Add on" is the operative word — every new project follows this exact same pipeline into /srv/projects/, out to /srv/www/ or a systemd service, and onto a Caddy route, alongside everything already live rather than replacing it.

Reading about the pipeline is one thing — watching it happen is another. This is that same workflow turned into a short animated walkthrough: a request lands in Telegram, an OpenClaw agent builds and deploys it live on this VPS, then the shipped result plays out.

RLabs Demo preview

RLabs Demo

live
From a text message to a live URL, animated — a Telegram request, an OpenClaw agent building and deploying on this VPS, then the shipped result.

Set this up yourself (quick version)

Never touched a server before? Here's the whole thing compressed to the main steps. You'll still need to look up specifics for your own provider/OS as you go — this is the map, not the turn-by-turn directions.

  1. Rent a VPS. Any provider works (Hetzner, DigitalOcean, Linode). Pick the cheapest Ubuntu LTS box — 2 vCPU / 4GB is plenty to start. You get root SSH access and an IP address.
  2. Buy a domain name. Any registrar (Namecheap, Hetzner, Google Domains). Point its DNS "A record" at your VPS's IP address — this is what makes yourdomain.com load your server instead of a random number.
  3. Install Caddy on the VPS. It's a web server that gets you free, auto-renewing HTTPS with almost no config — one line in a Caddyfile pointing your domain at a folder is enough to go live.
  4. Install OpenClaw. This is the agent framework that lets an AI (here, Claude, via Claude Code) run persistently on the box, connected to a chat app (Telegram, etc.) so you can give it instructions from your phone.
  5. Connect a Claude account/API key to OpenClaw so the agent actually has a model to think with.
  6. Talk to it. From here, the agent can write code, edit server config, and deploy — all from plain-language requests in chat, which is how everything on this site was built.

That's the skeleton. Each step above has its own setup docs from the provider/tool in question — worth reading those directly rather than guessing, since exact commands vary by OS and provider version.

TL;DR

You, in a browser
HTTPS request to ryantpham.com
Caddy, on the one Hetzner VPS
sole listener on 80/443 — routes by URL path
routes to whichever project the path matches
/  →  /srv/www/root
this site — Astro static build
/howimbuilt/  →  /srv/www/howimbuilt
static, this page
/<project>/  →  /srv/www/<project>
every other static project on the site

No database, no backend process, no login — plain files served straight off disk.

Architecture

Everything public flows through one firewall and one web server. The agent that maintains the box lives on a completely separate, private path — it never touches 80/443 and is not reachable from the internet.

System architecture A browser request passes through ufw and Caddy inside the public zone, then is served as static files or proxied to a dynamic app. A separate, unconnected private zone holds the OpenClaw agent gateway talking to Telegram, with no path in or out of the public zone. PUBLIC ZONE — ports 22, 80, 443 only PRIVATE ZONE — loopback only Internet ufw firewall 22, 80, 443 open — rest closed at the OS level Caddy sole listener on 80/443 Static files /srv/www/<project> served straight off disk Dynamic apps 127.0.0.1:70xx, systemd unprivileged, loopback-only no connection — agent is not reachable from the internet Agent gateway OpenClaw + Claude 127.0.0.1 only Chat interface Telegram single allowlisted sender

The stack

Host
Hetzner VPS, 2 vCPU / 4 GB
Cheap, predictable, full root access. Resizes in place without a rebuild.
OS
Ubuntu 26.04 LTS
Long support window; broadest compatibility with tooling.
Firewall
ufw
Default-deny inbound. Three ports open, nothing else.
Web server
Caddy 2
One process owns 80/443 and routes everything. Automatic HTTPS the moment a domain points here.
This site
Astro (static output)
Emits plain HTML. No runtime process, no memory cost, nothing to crash.
Content
JSON files
Structured data rather than markup, so it can be edited programmatically without degrading.
Shared app data
Flat JSON files
One file per project under a shared store, outside git, for projects that need state to persist across devices without a real database.
Runtime
Node.js
Powers the Astro build step, the secrets-proxy service (a small systemd-run Node process that keeps API keys server-side for static sites), and the OpenClaw agent gateway itself.
Agent
OpenClaw + Claude
Runs as a service on the same host; reached over a chat channel.

How a request is served

Caddy matches the most specific path first and falls through to the root site last. Static projects are served straight off disk; dynamic ones are proxied to a local port that is never exposed publicly.

redir /projectname /projectname/ 308
handle_path /projectname/* {
    root * /srv/www/projectname
    file_server
}

# dynamic equivalent
handle_path /appname/* {
    reverse_proxy 127.0.0.1:7001
}

The distinction that matters is handle_path rather than handle: it strips the path prefix before the request continues. Without that, an application mounted at /projectname receives /projectname/asset.css when it expects /asset.css, and every asset reference breaks.

Adding a project

The procedure is deliberately identical every time:

Nothing about this requires taking an existing project offline.

The agent layer

An OpenClaw gateway runs as a service on the same host, backed by a Claude model. It is reached through a messaging channel, which makes the deployment procedure above something that can be requested in a sentence rather than performed by hand.

This is the part with real teeth, so it is worth being precise about the trade-off. An agent that can deploy software is an agent with a shell. The controls that make that acceptable:

The honest caveat: instructions given to a language model are guidance, not a permission boundary. Content fetched from the open web is treated as untrusted, and anything that genuinely must not happen is enforced by file permissions and firewall rules rather than by asking nicely in a prompt.