How I got a public IP for my home server (almost for free) 

At some point, back when I was a broke student, I was hosting a bunch of stuff under my name. A few websites, some Telegram and Discord bots, and public dashboards for those bots so people could actually configure them. All of it needed to live somewhere on the internet.
The problem is, I was broke. Paying for proper hosting for every one of these projects was out of the question, so I had to get creative with my infrastructure.
The first piece was a tiny VPS I found on a deal for $1 a month. Its resources were a joke, but it had one thing I really cared about: unlimited traffic. The second piece was my dead old Macbook, which was just lying around doing nothing. I put Docker on it and moved every project there, each one in its own container. So now I had a machine with enough power to run everything, sitting at home.
And this brings us to the final problem. The Macbook was behind my home connection with a dynamic IP, and websites need an address that doesn't change every time the router feels like it. Meanwhile the $1 VPS had a static IP, but nowhere near enough power to run anything. So the question was how to put the Macbook behind the VPS's static IP. ![]()
Today a lot of people would just say "use a Cloudflare Tunnel" and be done with it. But my domain wasn't on Cloudflare, so I couldn't cheese the hard part that way. And to be frank, this story is old enough that I don't think Cloudflare Tunnel was nearly as well known back then as it is now. So I did it the old way.
The idea
The plan is pretty simple once you see it. The VPS becomes the public face of everything, and the Macbook does the real work behind it:
user -> VPS (static IP, nginx) -> WireGuard tunnel -> Macbook (10.66.66.2)The Macbook connects to the VPS through a VPN, so the two of them are on their own little private network no matter what my home IP is right now. When a request comes to the VPS, nginx passes it through the tunnel to the Macbook and sends the answer back. The VPS barely does any work, which is exactly why a $1 machine is enough. All it needs is the traffic, and the traffic was unlimited.
The VPN
For the tunnel I went with WireGuard. Setting it up by hand means generating keys and writing configs for both sides, and I didn't want to deal with any of that, so I used PiVPN. It's an installer that sets up WireGuard for you (and no, despite the name, you don't need a Raspberry Pi):
curl -L https://install.pivpn.io | bashAfter the install, pivpn add creates a profile for a client. I made one for the Macbook, copied it over, and the Macbook was now always reachable from the VPS at 10.66.66.2, wherever my home IP decided to go. That address is the one everything else points at.
Locking down the VPS
The VPS is the only part of this setup the whole internet can see, so it needs a firewall. I used ufw, since it's the least painful one. The idea is to only open the ports you really need: SSH, the WireGuard port, and 80/443 for the websites.
sudo ufw allow 22/tcp
sudo ufw allow 51820/udp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow in on wg0
sudo ufw allow out on wg0
sudo ufw enableLetting traffic through
By default, Linux won't pass packets from one network interface to another. A normal machine doesn't need that, but our VPS sits between the internet and the tunnel, so it has to. Turning on IP forwarding fixes that part (to keep it after a reboot, also set it in /etc/sysctl.conf):
sudo sysctl -w net.ipv4.ip_forward=1Then come a few iptables rules. They tell the VPS to forward traffic between the tunnel (wg0) and its internet-facing interface (ens5 in my case), and to rewrite the addresses with NAT, so replies find their way back:
sudo iptables -t nat -A POSTROUTING -o ens5 -j MASQUERADE
sudo iptables -A FORWARD -i wg0 -o ens5 -j ACCEPT
sudo iptables -A FORWARD -i ens5 -o wg0 -m state --state RELATED,ESTABLISHED -j ACCEPT
sudo netfilter-persistent saveFunny thing, on my VPS ens5 showed a private IP even though it was the interface facing the internet. The provider maps the public IP to it from the outside. It confused me for a while, so now you know.
nginx (reverse proxy)
nginx on the VPS listens for requests, looks at which domain they're for, and passes them through the tunnel to the right container on the Macbook. Every project gets its own server block. Say the main website runs on port 3000 on the Macbook and a bot dashboard runs on port 3001, one for domainexample.com and one for dashboard.domainexample.com:
server {
listen 80;
server_name domainexample.com;
location / {
proxy_pass http://10.66.66.2:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
server {
listen 80;
server_name dashboard.domainexample.com;
location / {
proxy_pass http://10.66.66.2:3001;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}Adding a new project is just another server block and a DNS record pointing at the VPS. That's honestly my favourite part of this setup.
For HTTPS, Certbot does everything. It gets a free Let's Encrypt certificate and edits the nginx config for you:
sudo apt install certbot python3-certbot-nginx
sudo certbot --nginxThe media files mystery
You might never run into this, so feel free to skip this section. But I did, and it cost me a lot of time (and sanity).
Everything seemed to work. Websites loaded, pages showed up, dashboards opened. But media files didn't load properly. Text was fine, but images and other heavier files just didn't come through.
What followed was a lot of tinkering with ChatGPT and hours of googling. Honestly, I don't even remember how I got to the answer in the end, but the problem was the MTU, the maximum size of a single network packet. WireGuard wraps every packet in its own extra header, so a packet that was fine before can become too big to fit through the tunnel. Small responses fit, so pages load. Big files get split into full-size packets, and those get lost on the way.
The fix is a single line in the [Interface] section of the Macbook's WireGuard config:
MTU = 13001300 worked for me, and I never touched it again. I've heard you can find a more optimal value for your own connection, but 1300 is the number that ended my suffering, so that's the one I'm giving you.
Summary
For $1 a month, my dead Macbook became a server that hosted all my websites, bots, and dashboards behind a static IP. It isn't the fanciest setup in the world, but it did the job for a broke student.
Also, even before that I was experimenting with hosting Discord bots who didn't required any static IP on my old phone, so this was a similar experience in obscurity. idk just felt like mentioning this would be a good idea :)
Well. That's it. Thanks for reading this small post ![]()