This site used to run on Vercel. It works well, it’s fast, and the developer experience is incredible. But Vercel is billed on usage, and usage can surprise you.
The bill problem
Riley Walz built Jmail, a Gmail-styled interface for searching the Epstein files. It went viral. 450 million views in a few days. The Vercel bill: $46,485.
A static site, HTML served from a CDN, billed at $46k.
The mechanics are: Next.js middleware runs on the edge on every request, not just page loads. Each request costs. When a story goes wide, that compounds fast. The Meta crawler alone has been responsible for billing spikes on otherwise quiet sites.
The free tier has a hard cap and kills your site. The Pro plan keeps it live and charges overages. That tradeoff is reasonable for a startup with revenue. It’s a risk for a personal project.
What 37signals proved
37signals spent $3.2 million on cloud in 2022. They bought $700k of Dell servers, migrated Basecamp and HEY off AWS, and recouped the hardware cost within the year. They now project $10 million saved over five years.
That’s a company with 80 employees and millions of users. The point isn’t that everyone should buy servers. The point is that managed cloud infrastructure has real costs that often go unexamined because billing is abstract until it isn’t.
For smaller projects the maths is simpler. A €5/month Hetzner VPS runs this site. The same traffic on Vercel Pro would be free until it wasn’t.
The self-hosting shift
Pieter Levels (@levelsio) has self-hosted everything for ten years. His site Remote OK loads in 27ms on a $99/month Hetzner VPS with Cloudflare’s free plan. Not edge functions, not a CDN with 300 PoPs and a metered invoice. One box, Cloudflare in front, done.
There’s a generation of tooling that makes this practical now. Docker makes your app reproducible. Dokploy gives you a deployment UI without the managed platform markup. Cloudflare handles the CDN and SSL for free. The operational complexity that used to justify paying for platforms has shrunk.
What this site runs on
The site is Astro with output: 'static'. Every page is pre-rendered HTML. No server, no functions, no runtime. Exactly the kind of workload that doesn’t need a managed edge network.
The setup:
- Hetzner VPS (Ubuntu 24.04, 4GB RAM, Helsinki) at around €5/month
- Dokploy for deployments, connected to GitHub
- Traefik (bundled with Dokploy) for routing and SSL via Let’s Encrypt
- Cloudflare in front, proxying and caching, free plan
The Dockerfile
Astro’s static output goes into an nginx container. The build stage installs dependencies and runs astro build. The final image is small and has no Node runtime.
FROM node:22-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
The nginx config handles Astro’s clean URLs by trying $uri, then $uri/, then $uri.html before falling back to the index. Without this, navigating directly to /about returns 404.
server {
listen 80;
root /usr/share/nginx/html;
index index.html;
location / {
try_files $uri $uri/ $uri.html /index.html;
}
location ~* \.(js|css|png|jpg|svg|woff2)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
}
Dokploy setup
Dokploy installs with a single script that sets up Docker, Traefik, Postgres, and Redis on your server. The full install took about two minutes.
curl -sSL https://dokploy.com/install.sh | sudo sh
After that, you create a project, connect your GitHub repo, point it at the Dockerfile, add your domain, and deploy. It handles the Traefik configuration and Let’s Encrypt certificates automatically.
The only friction with a private repo is authentication. I used a GitHub personal access token embedded in the clone URL, configured in Dokploy as the custom Git URL. Not ideal, but it works.
DNS
The original setup had danieljohnmorris.com pointing at Vercel’s IPs through Cloudflare. Switching was updating two A records to the Hetzner IP. Cloudflare’s SSL mode needs to be set to Full (not Flexible) so Cloudflare talks to Traefik over HTTPS rather than downgrading to HTTP.
The site was live on the new server within about ten minutes of changing the records.
Is it worth it?
For this site, yes. The infrastructure cost is €5/month. The same server runs other services. I have complete control over the deployment pipeline, the webserver config, and the caching behaviour.
The trade-off is operational responsibility. When something breaks, you fix it. Vercel’s support tier is not €5/month. If you’re running revenue-critical infrastructure, think carefully about that trade-off.
For personal projects and side work, a VPS and Dokploy is straightforward to operate.