Skip to content

Configuring a Reverse Proxy


I’ve written about reverse proxies before. Here’s a good one for nginx proxy manager.

Before we go any further, let’s explain what a reverse proxy does. It is, quite simply, a web server that serves web servers. Most reverse proxies can also be a standalone web server in their own right.

Instead of serving files over HTTP, a reverse proxy instead redirects traffic based on the domain you come from (see why domains are important?). They also usually have some neat tricks like being able to redirect to different locations depending on path, encrypting the traffic to the browser, load balancing, etc.

Most of those we don’t need. The two critical capabilities we leverage are:

  • Being able to serve legitimate encryption certificates via let’s encrypt
  • Being able to serve traffic based on the domain

Let’s get going!


In this guide, we will:

  • Register a domain/subdomain and point it at our IP
  • Forward Ports to our reverse proxy
  • Set up a reverse proxy with Caddy
  • Serve Headscale over HTTPS

Getting Your Domain Sorted

Domains aren’t anything special. You can make your own domains, just by editing C:\windows\system32\drivers\etc\hosts and adding custom dns entries.

If you want your domain to get encrypted properly though, you need a publicly recognized domain.


You can also technically set up a Public Key Infrastructure (PKI), but let’s not go there.

So you have to go to a registrar and go buy something to slap your name onto. Yes you have to pay for this, though it’s maybe 10 dollars a year for most domains. I recommend cloudflare. Point your domain’s DNS at your server’s IP address.


If you are behind Carrier Grade NAT (CGNAT) you can’t directly host public services. Use a Virtual Private Server (VPS) like linode or look into reverse proxy services like cloudflared.

In this case, I have a lab domain (don’t use my domain thanks, it won’t work for you), pointed at my lab environment’s IP.

Forward Your Ports

Once you have your domain sorted, you need to forward ports to your docker host. Specifically, ports 443 and 80. I’m going to do this in OPNSense


Port Forwarding should only be done if you know what you’re doing with security. You can avoid this by using a reverse proxy service (mentioned above) or DNS let’s encrypt challenges (but then headscale will only work inside your network, defeating the purpose)

Set up your Reverse Proxy

Did I mention we’re going to use Caddy? We’re going to use caddy. Feel free to swap out your own reverse proxy if you have a favorite, but caddy is nice and simple. We will also leverage docker networks to let the reverse proxy magic happen inside the docker environment.

  • Create a new network for your reverse proxy to sit on on the docker host.
docker network create reverseproxy-nw

  • Create a new folder for caddy and create a subfolder called container-config. Create a Caddyfile inside container-config with the following (changeing yourdomain with your actual domain: {
    reverse_proxy * http://headscale:8080


If that looks like a bizarrely simple config file, that’s because it is! That’s what’s great about caddy

  • Create a new docker-compose.yaml file in the caddy directory with the following:
    image: caddy:latest
    container_name: caddy
    restart: always
    stdin_open: true
    tty: true
      - ./container-data:/data
      - ./container-config:/config
      - /etc/localtime:/etc/localtime:ro
      - 80:80
      - 443:443
    entrypoint: /usr/bin/caddy run --adapter caddyfile --config /config/Caddyfile

    external: true

  • Bring up caddy with:
cd /mnt/containers/caddy
docker compose up -d


By default, caddy will attempt a let’s encrypt challenge over HTTP and HTTPS. This will not work if you haven’t done the domain and port forwarding steps, and you won’t get a website.

  • Alright, let’s test! Go to https://<your-domain>/windows and see if you’re serving headscale properly.

Hmm… No joy. Oh right! That’s because we need to add headscale to that new docker network so caddy can find it by the container name.

  • Redo the headscale docker-compose.yaml to resemble the following:
    container_name: headscale
    image: headscale/headscale:latest-alpine
    restart: unless-stopped
    # ports:
      # - 8080:8080
      - ./container-config:/etc/headscale
      - ./container-data:/var/lib/headscale
    entrypoint: headscale serve

    external: true

  • Also, update the headscale config.yaml file with your new URL

  • Rebuild headscale with:
cd /mnt/containers/headscale
docker compose up -d

  • Take Two!

That’s better.

Moving On

Alright, we have a headscale instance now, and we’re serving it properly on a domain with correct certificates. Now, let’s actually try connecting a client to it! in 03-configuring-tailscale-clients.