← Writing

Everything I run fits on one box

Four hostnames, one small server, and the hosting setup I use for personal projects.

This portfolio and the support sites for Sine, Slope, and Expences run on one Hetzner VPS. Four hostnames, one box, one docker compose up -d.

What runs there

The sites are static. They need HTML, CSS, a little JavaScript, and an nginx process to serve them. No database. No app runtime. No queue.

That keeps the setup simple enough for a small server. The edge nginx owns ports 80 and 443, handles TLS, and routes by hostname. Each site runs as its own container on loopback.

services:
  portfolio:
    image: ghcr.io/khan-tahseen/portfolio:latest
    restart: unless-stopped
    ports: ["127.0.0.1:8081:80"]

  sine:
    image: ghcr.io/khan-tahseen/sine-support:latest
    restart: unless-stopped
    ports: ["127.0.0.1:8082:80"]

Binding published ports to 127.0.0.1 matters. Docker can expose ports through firewall rules you did not expect. Loopback keeps the only public path through nginx.

server {
    server_name tahseenkhan.dev;

    location / {
        proxy_pass http://127.0.0.1:8081;
        proxy_set_header Host              $host;
        proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

Why each site is an image

Every site ships with its own nginx config. The edge server stays a router. Caching rules, redirects, and security headers live beside the site that needs them.

Site policy should move with the site, not hide in a shared server file.

The Docker build is two stages: Node builds the static files, then nginx serves the output. The production image has no build toolchain and no Node runtime.

What I got wrong

  • I first put too much policy in the edge nginx config.
  • I trusted header files without checking live responses.
  • I forgot that nginx add_header is not additive across blocks.

The fix was boring: keep each site self-contained and check headers after deploy.

The tradeoff

It is still one machine. If it dies, all four sites go down. That is fine for static support pages and a portfolio. It would not be fine for paid customer infrastructure.

For this use case, the setup is worth it. It is cheap, clear, and close enough to the metal that I actually understand what is running.