Field notes Infrastructure

Build your own
cloud network.

A public IP, a private network, and a little curiosity.
Let’s carve out your own corner of the Internet.

The Internet connects to your public VPS, then through an encrypted WireGuard tunnel to your home server and applications.
NetworkingWireGuardSelf-hosting
Let’s build something
On this page

Last Christmas, I bought my wife “Explain the Cloud Like I’m 10” after she mentioned, many times, how hard it was to relate to what I do every day at Qovery.

So far, I’ve been the sole reader to enjoy the book. But while reading, I wondered: are there any resources that explain how to build all that?

I reached for build-your-own-x, expecting to find some treasure to unearth, but ended up disappointed. Nothing really covered infrastructure or the cloud; most topics were software-oriented. So in today’s article, I’m going to explain how to build your own cloud network.

First, let’s make “the cloud” a little smaller.

The cloud covers scalability, reliability, availability, elasticity, on-demand resources, and much more. That’s too much for one article. Here, we’ll focus on the network: building your own place on the Internet, reachable by others.

From the archive. Adapted from the December 2023 PDF. Provider prices, product limitations, and service availability describe that period. The original illustrations and references are preserved.

01 / The foundations

The network plane

Okay, where do we start? Whenever you want to host your next startup idea, or a cool Rust project like Warpgate for a smart SSH and HTTPS bastion, you need an address. On the Internet, these are IP addresses, and they come in two flavors: IPv4 and IPv6.

An address of your own

Getting IPv4 space for yourself nowadays means putting a lot of money on the table. At the time of writing, IPv4 auctions offered nothing below $9,000 for a /24 subnet—a block of 256 addresses.

Why buy a whole block when you only need one address? By convention, people routing traffic on the Internet generally treat a /24 as the smallest globally routable IPv4 prefix.

It’s like your post office telling you that unless you live in a suburb with at least 256 houses, no postman will come to your mailbox.

If you’re starting today, invest some time in IPv6. Even if IPv4 never really dies, its small pool of remaining addresses is a problem. In 2023, AWS announced charges for public IPv4 addresses starting in 2024, passing those growing costs on to customers and encouraging a move to IPv6.

Thanks in part to mobile carriers, IPv6 was already well supported by major Internet players. All of them? Sadly, no. In 2023, GitHub still wasn’t IPv6-ready.

From the original: checking GitHub’s IPv6 support
A 2023 terminal screenshot showing a DNS query for GitHub with no IPv6 answer.
ARCHIVE The original IPv6 DNS lookup, captured in 2023.

Who hands out IP addresses?

That depends on where you are. Regional Internet Registries (RIRs) oversee the allocation and registration of IPv4 addresses, IPv6 addresses, and autonomous system numbers in their regions. If, like me, you’re in Europe, the addresses you use to browse the web are allocated through the RIPE NCC.

World map of the five Regional Internet Registries: ARIN in North America, LACNIC in Latin America, RIPE NCC in Europe and surrounding regions, AFRINIC in Africa, and APNIC in Asia-Pacific.
FIG. 02 The world’s five Regional Internet Registries. Original illustration from the PDF.

For an individual hobbyist, going directly to a registry isn’t the practical route. Fortunately, RIRs work with Local Internet Registries (LIRs); RIPE maintains a list of its members.

Contact an LIR, such as the service linked in the original guide, and ask for IPv6 address space. The next question is: which kind?

PI allocation

Provider independent

Independent of a provider’s address block, with more administration, fees, and requirements. The original guide treats this as the more involved route for someone building a business. See RIPE’s PI assignment process.

PA allocation

Provider aggregated

A range carved from the LIR’s own space and leased to you. The LIR owns the range, so you can’t take it with you, but you administer it. The guide’s 2023 estimate was around $100 a year.

Once you have your IPv6 range, how do you get the rest of the Internet to talk to you? Simple:

  1. Learn BGP.
  2. Find a provider that lets you peer with them to exchange routes.
  3. Announce your routes like the brave Internet citizen you are!
  4. You can even practice for free on DN42.

Wait. Isn’t that a little overblown? To host a simple blog, I need to spend hundreds of dollars a year, leave IPv4 users behind, and learn low-level BGP networking?

Well, yes. Maybe. But you did want to learn. ¯\_(ツ)_/¯

02 / A more practical path

Borrow a place on the Internet

Okay, okay. Owning an IP address range might be overkill. Where else can we get a public address? Like when you need a lighter and don’t have one: you ask someone else.

What about your ISP?

If you’re reading this, you’re already on the Internet, so you already have an IP address. You can find it on What Is My IP Address.

From the original: finding your public IP
An IP lookup page showing the author’s ISP-provided IPv4 and IPv6 addresses and approximate location in France.
ARCHIVE An ISP connection already gives you an address on the Internet.

It’s possible to start your cloud using your ISP’s address. But I don’t recommend it as your first step: there are more moving parts than necessary.

  • Your address might change. A dynamic address means setting up dynamic DNS with a service such as No-IP to keep your cloud easy to reach.
  • ISP networks filter traffic. Mail traffic is a common example: providers may block it to limit spam and infected machines. Policies differ between ISPs, leaving you to wonder why something doesn’t work.
  • You don’t fully control the router. The box between your home and the ISP might restrict features or configurations to reduce support needs and prevent abuse.
  • Your partner would like the Wi-Fi to keep working. They may care rather less about your learning adventure when it breaks the Internet. Your relationship will thank you for experimenting elsewhere.

So we need to stand on the shoulders of someone already established on the Internet, with a public endpoint that can route traffic to us.

Cloudflare: the easy way in

The path of least resistance in the original guide was Cloudflare Tunnel. There are already plenty of setup tutorials, so I won’t repeat all the details here.

In essence, you run a binary on your own server. It establishes a tunnel to Cloudflare’s infrastructure. Cloudflare then routes public traffic through that tunnel, making services on your internal network accessible from the Internet.

Cloudflare accepts user traffic and forwards it through a reverse tunnel, past the home router, to a web server and applications on the internal network.
FIG. 03 A reverse tunnel connects your private applications to a public entry point.

The original article highlights several tradeoffs: public web access centered on ports 80 and 443, restrictions around disproportionate non-web traffic such as images and video, and a dependence on Cloudflare’s tooling when you want to extend the setup. These are the author’s 2023 observations; product features and terms may have changed.

For more context, the original guide also links to this discussion of the downsides.

What if we could build the same idea ourselves, with the freedom to route whatever traffic we want?

03 / Your public entry point

Your gateway to the Internet

Now for the real deal. We’re going to create our own virtual private network. It takes some time, but we’ll be free to route different kinds of traffic and extend the network as our needs grow.

We still need someone to lend us a public IP address. The straightforward option is to rent a cheap Virtual Private Server (VPS) from a hosting provider.

The provider matters less than you might think: we’re mainly shuffling packets around. Pick a VPS near your own network. If your private network is in Paris, sending all its traffic through Sydney isn’t very efficient.

A world map illustrating long routes between Europe, North America, and Australia when a VPS is located far from the private network.
FIG. 04 Distance matters. Keep the trip between your public gateway and your home network short.

In 2023, I recommended Hetzner for affordable, reliable instances in Europe: roughly $5 per month for 2 vCPUs, 4 GB of RAM, and 20 TB of traffic. I’d also heard good things about Linode in the US. These are historical examples, rather than current price quotes.

From the original: the 2023 VPS pricing table
The original Hetzner cloud pricing screenshot, with its smallest displayed Arm instance priced at €4.51 per month in 2023.
ARCHIVE Provider pricing captured for the original article.

Unless you plan to run applications on the VPS—which we won’t in this guide—you don’t need to worry much about the CPU architecture. We just need Linux to move packets around.

Pick your VPS and install Linux. The rest of this guide uses Debian Bookworm. SSH into the machine and list its network interfaces:

VPS · terminal
ip a
Output from ip a on the VPS, annotated to identify its public IPv4 address 128.140.117.115 and IPv6 address 2a01:4f8:c012:32a1::1.
FIG. 05 Find the VPS’s public IPv4 and IPv6 addresses. Use your own addresses in the examples below.
Your first milestone.

You now have a public IP address: your gateway to the Internet and the entry point to your own cloud. From your local machine, try pinging both addresses.

From the original: a successful IPv4 and IPv6 ping
Successful ping responses from the public VPS over both IPv4 and IPv6, with zero packet loss.
ARCHIVE Both public addresses respond.
04 / The private side

A network with WireGuard

We’ll build our network with WireGuard: a simple, modern VPN with modern cryptography and a focus on performance. Once it’s in place, your applications will have a private path to the VPS. Then we’ll make them reachable from the Internet.

The target topology: an Internet user reaches the public VPS, which connects over WireGuard through a home router to the host running the application.
FIG. 06 The network we’re building. A public VPS connects to an application host inside your private network.

Our network has two subnets: one for IPv4 and one for IPv6.

NetworkPrivate subnetVPSHost
IPv410.42.0.0/1610.42.0.110.42.0.2
IPv6fd00:cafe::/32fd00:cafe::1fd00:cafe::2

The IPv4 block contains 65,536 addresses; the IPv6 block contains 296. Both are private ranges. These addresses won’t be routed on the public Internet. Visitors will see the VPS’s public address; the machines behind it stay inside your network.

Why use private IPv6 space when globally routable IPv6 addresses could avoid NAT? This is a beginner’s guide. Starting privately helps avoid unintentionally exposing your network through routing mistakes. It isn’t a dead end: you can later add a third, globally routable IPv6 subnet alongside the private ones.

Prepare the VPS

Open a root shell on the VPS. Refresh Debian’s package index, upgrade the installed packages, and install the tools we’ll use:

VPS · terminal
apt-get update && apt-get upgrade -y
apt-get install -y vim nano curl wireguard nftables

Give each peer an identity

WireGuard authenticates peers using public and private key pairs. We need one pair for the VPS and another for the machine running our applications.

VPS · /etc/wireguard
cd /etc/wireguard
# Restrict key files to their owner from the moment they are created.
umask 077
wg genkey | tee vps.private_key | wg pubkey > vps.public_key
wg genkey | tee my_cloud.private_key | wg pubkey > my_cloud.public_key

Keep each private key secret; only its machine needs it. Public keys can be shared with the other peers. The examples below use __VPS_PRIVATE_KEY__, __VPS_PUBLIC_KEY__, __HOST_PRIVATE_KEY__, and __HOST_PUBLIC_KEY__ in place of the key values printed in the PDF. Replace those placeholders with your own generated values.

Configure WireGuard on the VPS

Create /etc/wireguard/wg0.conf:

VPS · /etc/wireguard/wg0.conf
[Interface]
Address = 10.42.0.1/16, fd00:cafe::1/32
PrivateKey = __VPS_PRIVATE_KEY__
ListenPort = 995

# Linux must forward packets between the public and VPN interfaces.
PostUp = sysctl -w net.ipv4.ip_forward=1
PostUp = sysctl -w net.ipv6.conf.all.forwarding=1

[Peer]
PublicKey = __HOST_PUBLIC_KEY__
AllowedIPs = 10.42.0.2/32, fd00:cafe::2/128

Save and exit your editor (:x in Vim). Start the interface and inspect it:

VPS · terminal
wg-quick up wg0
ip a show wg0
ping -c 4 fd00:cafe::1

You should see a new wg0 interface with 10.42.0.1/16 and fd00:cafe::1/32. Its local IPv6 address should respond to the ping.

Example: WireGuard startup output
VPS · output
[#] ip link add wg0 type wireguard
[#] wg setconf wg0 /dev/fd/63
[#] ip -4 address add 10.42.0.1/16 dev wg0
[#] ip -6 address add fd00:cafe::1/32 dev wg0
[#] ip link set mtu 1420 up dev wg0
[#] sysctl -w net.ipv4.ip_forward=1
net.ipv4.ip_forward = 1
[#] sysctl -w net.ipv6.conf.all.forwarding=1
net.ipv6.conf.all.forwarding = 1

If you edit the configuration later, restart the interface with wg-quick down wg0 && wg-quick up wg0. To bring it up automatically after a reboot, enable its systemd unit:

VPS · terminal
systemctl enable wg-quick@wg0

# After the next reboot, systemd manages the interface.
systemctl status wg-quick@wg0

That’s the VPS side done. Time to connect your own machine.

05 / The other end of the tunnel

Connect your host

Your host can be anything that runs WireGuard: a Raspberry Pi, a ZimaBoard, a laptop, or a virtual machine on your laptop. In this guide, it’s another Debian Bookworm machine.

The VPS is configured; the next step is the application host, circled in green inside the private cloud network.
FIG. 07 The public end is ready. Let’s connect the private end.

Open a root shell on your host, update the system, and install WireGuard:

Host · terminal
apt-get update && apt-get upgrade -y
apt-get install -y vim nano curl wireguard

Create its configuration with the host’s private key and the VPS’s public key. Use the public address of your own VPS for Endpoint; it can be IPv4 or IPv6.

Host · /etc/wireguard/wg0.conf
[Interface]
Address = 10.42.0.2/32, fd00:cafe::2/128
PrivateKey = __HOST_PRIVATE_KEY__

[Peer]
PublicKey = __VPS_PUBLIC_KEY__
AllowedIPs = 10.42.0.0/16, fd00:cafe::/32
Endpoint = [2a01:4f8:c012:32a1::1]:995

# Keep the NAT mapping alive when the host sits behind a router.
PersistentKeepalive = 20

And now, the grand moment. Bring the interface up and ping the VPS using its private addresses:

Host · terminal
wg-quick up wg0
ping -c 4 10.42.0.1
ping -6 -c 4 fd00:cafe::1
Example: successful private-network pings
Host · output
64 bytes from 10.42.0.1: icmp_seq=1 ttl=64 time=20.4 ms
64 bytes from 10.42.0.1: icmp_seq=2 ttl=64 time=19.5 ms
64 bytes from 10.42.0.1: icmp_seq=3 ttl=64 time=16.4 ms
64 bytes from 10.42.0.1: icmp_seq=4 ttl=64 time=17.4 ms

4 packets transmitted, 4 received, 0% packet loss

64 bytes from fd00:cafe::1: icmp_seq=1 ttl=64 time=19.2 ms
64 bytes from fd00:cafe::1: icmp_seq=2 ttl=64 time=16.1 ms
64 bytes from fd00:cafe::1: icmp_seq=3 ttl=64 time=17.7 ms
64 bytes from fd00:cafe::1: icmp_seq=4 ttl=64 time=18.3 ms

4 packets transmitted, 4 received, 0% packet loss
You have a private network.

Your host and VPS can now talk over both IPv4 and IPv6, through the WireGuard tunnel.

Let’s try HTTP

Run a small HTTP server on your host. Use a separate directory for its content so you don’t accidentally serve the WireGuard configuration and its private key.

Host · terminal
mkdir -p /tmp/cloud-demo
printf 'Hello from my cloud!\n' > /tmp/cloud-demo/index.html
python3 -m http.server --bind :: 80 --directory /tmp/cloud-demo

From the VPS, request the host’s private address:

VPS · terminal
curl 'http://[fd00:cafe::2]'
# Hello from my cloud!

Hooray! HTTP works. But if you try the VPS’s public address, it fails:

VPS · terminal
curl 'http://[2a01:4f8:c012:32a1::1]'
# curl: (7) Failed to connect ... port 80: Couldn't connect to server

People arriving from the Internet still can’t see your app. Let’s fix that.

06 / Follow the packets

Let the traffic in

Private traffic successfully reaches the application host, but public HTTP traffic stops at the VPS because forwarding is not configured yet.
FIG. 08 The tunnel works. The missing piece is forwarding public traffic into it.

This time, we need the Linux kernel itself—specifically, its packet filtering features. Each received packet follows a series of hooks:

Linux packet flow through prerouting, a routing decision, input for local processes or forward for another host, and postrouting before an outgoing packet leaves.
FIG. 09 The original packet-flow diagram, rejoined from two PDF pages.

When a request hits the VPS’s public address, the kernel decides that the traffic is for this host. It tries to deliver the packet to a local process listening on port 80. Unfortunately, that process is in another castle: our private host.

We need the kernel to take the forward branch instead. We’ll use nftables, the modern alternative to iptables, to change the packet’s destination before that routing decision.

InternetPUBLIC REQUEST
VPSDNAT + WIREGUARD
Your applicationPRIVATE HOST

The firewall configuration

Write the following to /etc/nftables.conf on your VPS. The example uses eth0 as its public interface. Replace it if your interface has a different name.

The rules allow SSH and WireGuard on the VPS, forward ports 80, 443, and 2222 to the host, and masquerade the forwarded connection so replies return through the tunnel.

VPS · /etc/nftables.conf
#!/usr/sbin/nft -f
flush ruleset

table inet SERVER_FIREWALL {
    set blacklisted_ips {
        type ipv4_addr;
        flags dynamic, timeout;
        timeout 5 m;
    }

    chain input {
        type filter hook input priority 0; policy drop;

        ct state invalid drop comment "early drop of invalid packets"
        ct state { established, related } accept

        iif lo accept comment "accept loopback"
        iifname wg0 accept comment "accept wireguard traffic"

        # ICMPv6 is needed for IPv6 operation, not only ping.
        meta l4proto icmp accept
        meta l4proto ipv6-icmp accept
        udp dport 546 ip6 saddr { fc00::/7, fe80::/10 } accept

        tcp dport 22 accept comment "accept local SSH"
        udp dport 995 accept comment "accept wireguard"
    }

    chain nat_prerouting {
        type nat hook prerouting priority 0; policy accept;

        # Original example: a shared rate limit and an IPv4 blacklist.
        tcp dport { 22, 2222 } ct state new, untracked \
            limit rate over 5 / minute \
            add @blacklisted_ips { ip saddr }
        ip saddr @blacklisted_ips drop

        # Change the destination before the kernel decides where to send it.
        iif eth0 tcp dport { 80, 443, 2222 } dnat ip to 10.42.0.2
        iif eth0 tcp dport { 80, 443, 2222 } dnat ip6 to fd00:cafe::2
    }

    chain forward {
        type filter hook forward priority 0; policy drop;

        iifname wg0 accept comment "accept forwarding wireguard traffic"
        oifname wg0 accept comment "accept forwarding wireguard traffic"
    }

    chain nat_postrouting {
        type nat hook postrouting priority 0; policy accept;

        # The host must reply through the VPS instead of its default gateway.
        iif eth0 oifname wg0 masquerade
    }

    chain output {
        type filter hook output priority 0; policy accept;
    }
}

Why masquerade?

Changing the destination gets an incoming packet to your host. But the host also needs a return path. With our split tunnel, replies to an arbitrary Internet client would otherwise follow the host’s usual default route.

The masquerade rule replaces the client’s source address with the VPS’s address on the outgoing WireGuard interface. Your host replies to that private address, and connection tracking reverses the translation on the way back. The public client sees a response from the VPS.

A detail that matters.

Masquerading here makes the application see the VPS’s private address as its client. The example also starts with flush ruleset, replacing the VPS’s existing nftables rules. It is the original tutorial setup for a dedicated gateway.

Apply the rules on the VPS:

VPS · terminal
chmod +x /etc/nftables.conf
/etc/nftables.conf

Now, from your host or another Internet-connected machine, request the VPS’s public addresses. Substitute your own VPS addresses:

An Internet-connected machine · terminal
curl 'http://[2a01:4f8:c012:32a1::1]'
# Hello from my cloud!

curl http://128.140.117.115
# Hello from my cloud!

Hooray for IPv6. Hooray for IPv4. You can now route public traffic to a machine inside your own cloud network.

The finished route: an Internet user reaches the public VPS, and traffic is forwarded across WireGuard to the application in the private network.
FIG. 10 The complete path, from an Internet user to your privately hosted application.

Run something useful

Once you install Docker on the host, you can run an application in a container. For example, here is the Warpgate setup from the guide, with the image name’s printing errors corrected:

Host · terminal
docker run --rm -ti \
    -p 443:8888 -p 2222:2222 \
    -v /mnt:/data \
    ghcr.io/warp-tech/warpgate setup

docker run --rm -ti \
    -p 443:8888 -p 2222:2222 \
    -v /mnt:/data \
    ghcr.io/warp-tech/warpgate

Reach Warpgate on the HTTPS port of your VPS, accepting the self-signed certificate for this example.

The Warpgate welcome and login screen reached through the public IPv6 address of the VPS.
FIG. 11 An application in your private network, now reachable through the public gateway.

Bravo! Enjoy your moment of triumph—but don’t let it go to your head. There’s still plenty to learn in the networking world.

07 / Room to grow

One network, more machines

I won’t go into every detail, but there are three ways to extend your private network.

1. Connect every machine to the VPS

Connect the next host exactly as you connected the first. This works well, but traffic between hosts has to go out to the VPS and back into your network. For lots of local communication, that’s a lot of unnecessary back and forth.

Two private hosts connect independently to the VPS. Traffic between them travels out through the VPS and back into the private network.
FIG. 12 A hub-and-spoke setup. Simple to extend, but local traffic takes the long way around.

2. Add a gateway inside your network

If machines talk to one another frequently, keep that traffic local. Run another WireGuard gateway inside the private network and configure it to route traffic for the other hosts. You can do all of this with WireGuard.

A private host acts as a local WireGuard gateway, connecting the VPS to another internal host while keeping internal traffic local.
FIG. 13 A local gateway keeps traffic close to home.

The host acting as a gateway would have a configuration like this:

Gateway host · /etc/wireguard/wg0.conf
[Interface]
Address = 10.42.0.2/16, fd00:cafe::2/32
ListenPort = 995
PrivateKey = __HOST_PRIVATE_KEY__

# This host now routes packets for the peers behind it.
PostUp = sysctl -w net.ipv4.ip_forward=1
PostUp = sysctl -w net.ipv6.conf.all.forwarding=1

[Peer]
PublicKey = __VPS_PUBLIC_KEY__
AllowedIPs = 10.42.0.1/32, fd00:cafe::1/128
Endpoint = [2a01:4f8:c012:32a1::1]:995

[Peer]
PublicKey = __NEW_HOST_PUBLIC_KEY__
AllowedIPs = 10.42.0.3/32, fd00:cafe::3/128

And on the VPS:

VPS · /etc/wireguard/wg0.conf
[Interface]
Address = 10.42.0.1/16, fd00:cafe::1/32
PrivateKey = __VPS_PRIVATE_KEY__
ListenPort = 995
PostUp = sysctl -w net.ipv4.ip_forward=1
PostUp = sysctl -w net.ipv6.conf.all.forwarding=1

[Peer]
PublicKey = __HOST_PUBLIC_KEY__

# The gateway host routes traffic for the rest of the private network.
AllowedIPs = 10.42.0.0/16, fd00:cafe::/32

3. Build a full mesh

Every machine connects directly to every other machine. The route is efficient, but the setup is more involved: adding a host means updating the configurations on all the others. Personally, I find that tedious.

A triangular full-mesh network with direct WireGuard connections between the VPS and both private hosts, and a direct connection between the hosts.
FIG. 14 A full mesh trades more configuration for direct paths between every peer.
Looking for something to run?

The original guide recommends CasaOS as a way to install applications on your host and have some fun with your new setup.

08 / Just the beginning

A small cloud. A lot of possibility.

We’ve walked through the steps needed to build your own cloud network, reachable from the Internet. A public address, a private tunnel, a little routing—and your applications have a home.

Everything is manual at this point, and there are still single points of failure. To go further, you could make the NAT gateway redundant with two VPSs and a floating IP, or introduce automation and testing to configure and deploy your WireGuard tunnels.

This might feel like a toy example, but the broader pattern—private machines behind controlled public entry points—is also useful in larger cloud infrastructure. The original article draws a comparison with AWS private networks and NAT gateways for controlling outbound traffic, while inbound access needs its own entry point.

Even on AWS, building that network takes configuration. At Qovery, the original guide’s example made the cluster’s network setup available in a click, without all this manual fiddling.

This was the network plane. Maybe next time, we’ll explore the data plane.

Hope you enjoyed the journey. Now go build something. :)

About this edition

This web edition follows Build your own cloud network, exported to PDF on 12 December 2023. Its 26 pages have been reflowed into one article, with the original references and all 17 substantial figures. Three figures split across pages have been rejoined.

Prose has been lightly edited for readability, repeated page-break text removed, and repetitive terminal output condensed. Printed private keys are replaced with placeholders; key files are created with restricted permissions, and the HTTP demo serves a separate directory. Obvious address and image-name typos are corrected. The /24 address count, masquerading explanation, rate-limit comment, and AWS comparison have been clarified. The PI allocation discussion is condensed and points to the original RIPE reference.

The examples retain the original Debian Bookworm and 2023 context. This is a formatted archival article, not a newly tested infrastructure deployment.

Written by Romain Gérard

Software engineer. Curious about the systems beneath the software.
More about me ↗  ·  Find me on GitHub ↗