# Using a Public Domain to Access Private Services

> In the previous article, I explained how to configure a Mac mini homelab and use Tailscale to connect to it remotely. Although Tailscale provides MagicDNS, you still have to access services using the MagicDNS:port format. This article walks through, step by step, how to use a public domain with HTTPS to access services running inside your private network.

## The limitation of Tailscale

Imagine the following setup:

* Two Docker services:
  * **Service A** running on port `3000`
  * **Service B** running on port `3001`
* A domain `abc.com` managed by **Cloudflare**
* A public VPS with an IPv4 address

In a traditional setup, you would expose these services like this:

* `a.abc.com` → Service A
* `b.abc.com` → Service B

You’d add DNS records in Cloudflare and use **Nginx** to reverse-proxy traffic to the corresponding services.

![Jietu20260126-182953@2x](https://static.yancey.app/8e7kds8n-1769423427419.jpg)

## What Changes with Tailscale?

Tailscale provides **MagicDNS**, for example:

```bash
homelab.xxxx.ts.net
```

However, MagicDNS **does not support wildcard subdomains**, which means:

You **cannot** access services via:

* `a.homelab.xxxx.ts.net`
* `b.homelab.xxxx.ts.net`

Instead, Tailscale offers the `serve` feature, which forces you to use:

* `homelab.xxxx.ts.net:3000`
* `homelab.xxxx.ts.net:3001`

Technically it works—but let’s be honest: **It’s ugly.**

![Jietu20260126-174228](https://static.yancey.app/t3dxpl5q-1769422932370.jpg)

## Can We Use a Public Domain?

Yes—**but with a catch**.

You can configure a wildcard subdomain in Cloudflare, such as:

```bash
*.homelab.abc.com
```

and point it to your homelab’s **private IP address** (reachable only through Tailscale).

With this setup, once the Tailscale tunnel is active, you can access:

* `a.homelab.abc.com`
* `b.homelab.abc.com`

via a reverse proxy (Nginx or Caddy).

Sounds perfect, right?

![Jietu20260126-183842](https://static.yancey.app/iowtgbpo-1769423935178.jpg)

## The HTTPS Problem

There’s one critical issue.

Modern browsers **strongly discourage** (or outright block) visiting websites without **HTTPS**.

You might think:

>  “No problem, I’ll just get a Let’s Encrypt certificate.” 

Unfortunately, that won’t work.

Let’s Encrypt **will not issue certificates** for:

* Private IP addresses
* Non-publicly routable endpoints (like Tailscale IPs)

So how do we get HTTPS?

## Using a Cloudflare API Token

The solution is **DNS-01 validation** using Cloudflare’s API. Go to: https://dash.cloudflare.com/profile/api-tokens and click **Create Token**.

![Jietu20260126-185900](https://static.yancey.app/i5xz8oxi-1769425359748.jpg)

Use the **Edit zone DMS** template.

![Jietu20260126-185916](https://static.yancey.app/hzid9zf1-1769425422718.jpg)

Set the permissions as follows:

**Permissions**

* Zone → Zone → **Read**
* Zone → DNS → **Edit**

**Zone Resources**

* Include → **All zones**

Finish the setup and **save the generated API token**. Important: **This token is shown only once—store it securely.**

![Jietu20260126-190004](https://static.yancey.app/huk82kad-1769425503333.jpg)

## Reverse Proxy Your Services (with Caddy)

This time, we’ll use **Caddy v2** instead of Nginx, because Caddy handles HTTPS automation extremely well.

Create the following structure:

```bash
.
├── caddy_config/
├── caddy_data/
├── Caddyfile
└── docker-compose.yml
```

![Jietu20260126-191720@2x](https://static.yancey.app/5btlpsmf-1769426283466.jpg)

## docker-compose.yml

Paste your **Cloudflare API token** into the environment section and start Caddy with: `docker-compose up -d`

```bash
services:
  caddy:
    image: caddy:latest
    container_name: caddy
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    environment:
      - CLOUDFLARE_API_TOKEN=<YOUR_CLOUDFLARE_API_TOKEN>
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
      - caddy_data:/data
      - caddy_config:/config

volumes:
  caddy_data:
  caddy_config:
```

## Install the Cloudflare DNS Module

The official Caddy image does not include the Cloudflare DNS module by default. Run the following commands:

```bash
docker exec -it caddy sh
caddy add-package github.com/caddy-dns/cloudflare
```

## Tailscale DNS Configuration (Important)

Before configuring Caddy, go to **Tailscale Admin Console** and add public DNS resolvers:

* **Cloudflare DNS**: `1.1.1.1`
* **Google DNS**: `8.8.8.8`

Path:

```bash
DNS → Global nameservers
```

This ensures DNS-01 validation works correctly inside the Tailscale network.

![Jietu20260126-193300](https://static.yancey.app/rrlns6rr-1769427267595.jpg)

## Configure the Caddyfile

Below is a minimal example.\
Visiting `a.homelab.abc.com` will return a static response:

```bash
{
        acme_dns cloudflare {env.CLOUDFLARE_API_TOKEN}
}

a.homelab.abc.com {
	respond / "Hello, world! This is a static response from Caddy." 200
}
```

Once this is working, you can replace `respond` with `reverse_proxy` to point to your actual services.


---

Source: https://yanceyleo.com/post/d3fba50c-a4ff-4971-8cb1-1f6fdff35a7c
Published: 2026-01-26
Tags: Homelab, TailScale, Cloudflare