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_headeris 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.