Field Guide // 001

Host your site
from a box
on your desk.

A step-by-step walkthrough for self-hosting a static Astro site on an isolated VM — with a hardened setup, no SSH, and a real plan for network isolation.

Static site Isolated VM No SSH Nine phases DIY sysadmin
The mission

What you're actually building.

Before the steps: a quick reality check on what this setup gives you and why it works.

!

You're putting a machine on the public internet. That's a real responsibility — but your posture is inherently safer than most self-hosting scenarios because you're doing three specific things right.

One: the site is static. No PHP, no database, no login page. Almost all compromised web servers get compromised through the application, not the web server itself. You've removed that entire category of risk.

Two: no SSH. You skipped the single most-attacked service on internet-facing Linux boxes. Every service you don't run is a service that can't be exploited.

Three: the VM lives on its own network. If something bad happens to the server, "inside your house" doesn't mean "inside your network." The blast radius is contained.

Gear check

The stack.

Six decisions to make before you start. There are no wrong answers here — these are the defaults that will bite you the least.

01 / Operating system

Ubuntu Server LTS

Long support windows, huge community, most tutorials assume it. Debian is equally fine if you prefer minimal.

Alt: Debian, Fedora Server, Alma
02 / Web server

nginx or Caddy

nginx has the most current docs. Caddy auto-manages TLS certificates for you — genuinely great if you want to skip that chore.

Alt: Apache (fine, more legacy)
03 / TLS certificates

Let's Encrypt via certbot

Free, automated, industry-standard. Renew every 60 days with a systemd timer. Caddy handles this without any config.

Alt: acme.sh, or Caddy's built-in
04 / Firewall

ufw

Friendly frontend to nftables. Allow 80 and 443 inbound, deny everything else. Since you're not running SSH, don't open 22.

Alt: firewalld, raw nftables
05 / Domain

Any registrar

Porkbun and Cloudflare Registrar sell at-cost. Point the A record at your public IP. Dynamic DNS if your IP changes.

Alt: Namecheap, Namesilo
06 / Deploy method

Git pull on the VM

Without SSH, deploys happen from the console. Push to a repo, pull from the VM, build locally. Or fetch a CI artifact tarball.

Alt: Shared folder, CI artifact
P.S. // The site stack

And on the site side — what you're actually deploying.

The server hosts what these three tools produce. Worth keeping the two stacks separate in your head: one builds the site, the other runs it.

npm

Package manager for the Node.js ecosystem. Installs Astro, Tailwind, and their dependencies.

Astro

Static site framework. Turns your Markdown and .astro files into HTML at build time.

Tailwind CSS

Utility-first styling. Pre-built classes you apply directly in your markup — no separate stylesheet to write.

The topology

How the network fits together.

The whole security model depends on this shape. Traffic from the internet reaches the VM, but the VM cannot reach — and is not reachable from — anything on your regular LAN.

              ┌─────────────────────────────┐
                        INTERNET           
              └──────────────┬──────────────┘
                             
                               ports 80, 443
                             
              ┌─────────────────────────────┐
              │           ROUTER            │
              │      (WAN port forward)     │
              └─────┬─────────────────┬─────┘
                                     
        isolated subnet       home LAN
                                     
      ┌─────────────────────┐   ┌────────────────────┐
      │   ▓▓▓ THE VM ▓▓▓    │   │      YOUR LAN      │
      │                     │   │                    │
      │   nginx / caddy     │   │   laptop / phone   │
      │   ufw + certbot     │   │   printer / NAS    │
      │   Astro dist/       │   │   everything else  │
      └──────────┬──────────┘   └─────────┬──────────┘
                                         
                 └────────  ✗  ───────────┘
                    FIREWALLED APART

The VM reaches the internet. The internet reaches the VM. The VM does not reach your LAN — and your LAN does not reach the VM, except through whatever admin path you deliberately define.

The build

Nine phases, in order.

Each phase is self-contained. Get it working before moving to the next. That way, when something breaks, you know it's the new thing that broke — not something three steps back.

01
Boot the machine

Get the VM running.

Install Ubuntu Server LTS in your hypervisor of choice (VirtualBox, VMware, Proxmox, Hyper-V, whatever you already have). Give it 1–2 GB of RAM, one CPU, and 10–20 GB of disk — plenty for a static site. Boot, log in via the hypervisor console, and get comfortable in the shell.

Note Don't enable an SSH server during the install prompts. That's the whole point — you're managing this through the console.
02
Serve a byte

Get the web server running on the LAN.

Install your web server and confirm you can hit a page from your laptop — while everything is still on your normal network. Prove the pieces work in isolation before you start layering complexity.

$ sudo apt update
$ sudo apt install nginx
$ echo "it's alive" | sudo tee /var/www/html/index.html
$ ip addr  # find the VM's IP

Open http://[vm-ip] from your laptop's browser. If you see your message, the web server is working. Nothing to do with the internet yet — just LAN.

03
Close the doors

Turn on the firewall.

Only two ports need to be open to the outside world. Everything else stays closed.

$ sudo ufw allow 80/tcp
$ sudo ufw allow 443/tcp
$ sudo ufw enable
$ sudo ufw status

Verify the site still loads from your laptop. If it does, ufw is doing its job — allowing web traffic and blocking the rest.

04
The blast radius

Move the VM to its own network.

This is the setup-dependent step. The goal: the VM should be able to reach the internet, but your LAN and the VM shouldn't be able to reach each other. Pick the option that matches your gear:

  • VLAN on a smart router (OpenWrt, pfSense, OPNsense, UniFi) — cleanest, firewall rules enforce isolation.
  • Guest network if your router has one — already isolated from the main LAN by design.
  • Double-NAT via the hypervisor — a weaker fallback. LAN can't reach the VM, but you'll need explicit firewall rules to stop the VM reaching the LAN.
Verify it From the VM, try to ping your laptop. From your laptop, try to ping the VM. Both should fail. Then curl a public site from the VM — that should succeed. If any of these are wrong, the isolation isn't working yet.
05
Name in lights

Get a domain and point DNS.

Buy a domain from any registrar. In the DNS settings, create an A record pointing your domain (and the www subdomain, if you want) at your home's public IP address. You can find your public IP by searching "what is my ip" from any device on your home network.

If your IP changes Most residential ISPs give you a dynamic IP that changes occasionally. Set up dynamic DNS — a small script (or a service like DuckDNS) that updates the DNS record whenever your IP changes.
06
Open the pipe

Forward the ports on your router.

In your router's admin interface, add port forwarding rules: WAN port 80 → the VM's private IP on port 80, and WAN port 443 → the VM's private IP on port 443. The exact UI is different for every router; look for "Port Forwarding" or "Virtual Servers."

Once the rule is live, your site is reachable from the internet — at least on HTTP. Try loading http://yourdomain.com from your phone (on cellular, not WiFi, so it actually goes out and back). If you see the page, the whole path works.

07
Padlock, please

Add TLS with Let's Encrypt.

Now that your domain resolves and port 80 is reachable, certbot can prove you own the domain and issue a real certificate. One command handles everything — the cert, the nginx config edit, and the auto-renewal timer.

$ sudo apt install certbot python3-certbot-nginx
$ sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com

After it finishes, https:// works and HTTP automatically redirects. Certbot installs a systemd timer that renews the cert every 60 days.

Using Caddy? Skip this whole step. Caddy handles Let's Encrypt automatically the first time it sees a domain in its config. Nothing to run, nothing to renew.
08
The payload

Deploy your Astro site.

Without SSH, you need a way to get files onto the VM without opening remote access. A few workable patterns:

  • Build on the VM. Install Node or Bun, clone your repo, and run npm run build right there. Point nginx at the resulting dist/ folder.
  • Fetch a built artifact. Build on GitHub Actions, publish a tarball as a release asset, and curl it onto the VM. The VM only needs to fetch — no push access required.
  • Shared folder. If your hypervisor supports it, share a folder with the host and build there. Simple, but couples the VM to the host machine.

Point nginx at the built folder in /etc/nginx/sites-available/default — change the root directive to your dist/ path, then sudo systemctl reload nginx. Reload the page and your Astro site is live.

09
Long-term care

Automate updates and check the logs.

You're a sysadmin now. Two habits keep the machine healthy without much ongoing effort.

$ sudo apt install unattended-upgrades
$ sudo dpkg-reconfigure --priority=low unattended-upgrades

That takes care of security patches. For monitoring, occasionally skim journalctl -u nginx and /var/log/nginx/access.log. You'll see relentless bot traffic probing for exploits — that's the normal internet. What you're looking for is anything unusual: a spike in 500 errors, requests hitting paths that don't exist on your site, patterns that suggest an actual attack.

Reality check The instant port 80 opens, bots start scanning. Most of it is noise, but it's a good reminder to keep patched. Your static site has almost no attack surface — but the web server itself is exposed, and the OS underneath it. Automated updates matter more than anything else you do.

You shipped it.

Nine phases, one running server, one working site. You built the whole thing yourself — the OS, the network, the web server, the TLS, the deploy pipeline.

The site itself is the smaller half of what you just learned. The bigger half is the mental model: how a request travels from a browser, through the internet, past a firewall, into a process, and back out as HTML. That's the model that makes every future infrastructure decision easier.