<- All projects

This Portfolio

A Next.js portfolio self-hosted on bare metal behind Cloudflare Tunnel. No PaaS, zero open ports, and $0/month in cloud hosting bills.

SanityTailwind CSSshadcnNext.jsReactNode.jsTypescriptDocker
3 min readSource Live

Every personal site starts the same way: scaffold a framework, deploy it on a managed platform, and call it a day. It’s clean, it’s fast, and it works. But this one had a hard constraint attached before I even wrote a single line of code.

No PaaS and no open ports

This site runs on a single machine I own, running Cloudflare Tunnel. Nothing is exposed directly to the public internet, no home router ports are punched open (it's impossible because of CGNAT anyway), and nothing depends on a hosting provider’s generous “free tier” suddenly turning into a paid invoice. Besides, I needed to be smart with my money, and frankly, watching an idle piece of hardware gather dust while paying someone else for compute feels like a personal insult.

The Economics

Modern hosting providers depend that their customers are both lazy and rich. They secretly hope that I made a slip-up that costs me a monthly paycheck. I'm neither.

Why pay $20/mo. for a hobby compute when you can just run it on electricity that already costs less than a cup of coffee?

Tunneling

You know it, it's always tunneling, but it's a real lifesaver for people behind CGNAT.

Even without CGNAT, you would have to mess with the router, port forward, set up a dynamic DNS because my IP changes, and actually listen at that port. It's tiring, and it can be dangerous when you become lazy and port forward literally everything, and when a random, vulnerable process listens to an open port and get your computer hacked, game over.

With cloudflared, it creates an outbound tunnel to Cloudflare's server. No open ports, and no ISPs being aware that I'm running a server.

CI/CD

CI/CD sounds like a buzzword, which it is. It usually conjures images of bloated enterprise pipelines, 45-minute builds, and people with "DevOps Evangelist" in their bio arguing about Kubernetes clusters.

For a personal site, setting up a full pipeline feels like using a forklift to carry a carton of eggs. The alternative is so tempting: just ssh into the box, run git pull, run npm run build, and hope for the best.

But we all know how it ends.

When there's a critical vulnerability in your site, and you're in vacation, you can't fix the vulnerability. And even at home, you're on your laptop SSH-ing to your server and doing an in-place edit, which may or may not be pushed to origin.

With working CI/CD, you can fix it with a single git push. It can automate linting, testing, building, and deploying.

You may ask: "how does a CI/CD pipeline access my PC without open ports," to which I say, there is a solution. GitHub Actions Self-Hosted Runner. Instead of GitHub accessing my server, my server calls out to GitHub over a persistent outbound connection.

To make it easy, I use GitHub Actions's basically infinite CI/CD compute to lint, test, build images, and deploy for free. With the built images, it's pushed to GHCR. Then, my local server pulls from GHCR, restores the secrets, starts up docker, and run basic checks.

The result? Easy deploy. Easy checks. Identical to Vercel.

Challenges

Needless to say, there are challenges.

  • Port Collision: I realized that port 8080 was being used by mod-oud, so instead of suffering, I changed the port.
  • The Disappearance of .env: git clean -ffdx deletes untracked files and folders, which includes .env, so I placed the .env at my home directory as ~/.portfolio.env and copied it to the local repo.
  • Trust, but verify: Checking Docker healthchecks and curling the actual external tunnel domain before celebrating.