Tech Expert & Vibe Coder

With 15+ years of experience, I specialize in self-hosting, AI automation, and Vibe Coding – building applications using AI-powered tools like Google Antigravity, Dyad, and Cline. From homelabs to enterprise solutions.

Configuring Caddy automatic HTTPS with Tailscale certificates for internal services without exposing ports to the internet

Why I Needed This

I run multiple services on my home network—mostly Docker containers for personal projects, monitoring tools, and automation workflows. For a long time, I accessed them by IP and port, which meant:

  • No HTTPS unless I manually generated certificates
  • Browser warnings every time I opened a dashboard
  • No remote access without opening ports or setting up VPNs

I wanted clean, secure HTTPS access to these services from anywhere, but without exposing anything directly to the internet. I didn’t want to deal with Let’s Encrypt DNS challenges or maintain certificate renewal scripts.

That’s when I looked into Caddy’s Tailscale integration. Caddy 2.5+ can automatically fetch certificates from Tailscale for *.ts.net domains, and since I was already using Tailscale for remote access, this seemed like the right fit.

My Setup

I’m running this on a Proxmox VM with Docker and Docker Compose installed. The VM is part of my Tailscale network (tailnet), which means it gets a machine-name.tailnet-name.ts.net hostname automatically.

The core components are:

  • Tailscale daemon running on the host (not in a container)
  • Caddy in a Docker container, configured to use Tailscale certificates
  • Backend services in their own containers, connected via a Docker network

Caddy acts as a reverse proxy. It listens on port 443, terminates HTTPS using Tailscale-issued certificates, and forwards requests to backend containers over HTTP.

Docker Compose Structure

I use a single docker-compose.yaml file that defines:

  • A custom Docker network called proxy-network
  • The Caddy container with access to Tailscale’s socket
  • Backend service containers (like n8n, Uptime Kuma, etc.)

The key part is mounting Tailscale’s socket into the Caddy container:

volumes:
  - /var/run/tailscale/tailscaled.sock:/var/run/tailscale/tailscaled.sock

This lets Caddy talk directly to the Tailscale daemon to fetch certificates without needing root access or complicated permissions.

Caddyfile Configuration

My Caddyfile is minimal. For each service, I define a block like this:

service.machine-name.tailnet.ts.net {
    reverse_proxy backend-container:8080
}

Caddy automatically detects the .ts.net domain and requests a certificate from Tailscale. No additional configuration needed.

I also have a catch-all block for the root domain that serves a simple status page:

machine-name.tailnet.ts.net {
    respond "Caddy is running" 200
}

This helps me confirm the setup is working before adding more services.

What Worked

Once I got the socket mount and network setup correct, everything clicked into place. Caddy fetched certificates automatically on the first HTTPS request to each subdomain. No manual steps, no DNS configuration, no waiting for propagation.

The certificates are valid for 90 days and renew automatically. I’ve been running this for months now without touching it.

Remote access works seamlessly. As long as I’m connected to Tailscale on my phone or laptop, I can open https://service.machine.tailnet.ts.net and it just works—no browser warnings, no port forwarding, no firewall rules.

Non-Root User Permissions

Initially, Caddy couldn’t access the Tailscale socket because it was running as a non-root user inside the container. I fixed this by setting the TS_PERMIT_CERT_UID environment variable in /etc/default/tailscaled on the host:

TS_PERMIT_CERT_UID=caddy

Then I restarted the Tailscale daemon:

sudo systemctl restart tailscaled

After that, Caddy could fetch certificates without needing elevated privileges.

What Didn’t Work

My first attempt failed because I tried running Tailscale inside a container alongside Caddy. The official Tailscale Docker image exists, but getting it to share state with Caddy in a way that allowed certificate fetching was messy. I ended up with permission errors and stale socket connections.

Running Tailscale on the host and mounting the socket into Caddy’s container was far simpler and more reliable.

Another issue: I initially forgot to add backend containers to the proxy-network. Caddy couldn’t resolve their hostnames, and I got 502 errors. The fix was making sure every service container joined the same Docker network.

Certificate Fetch Delays

The first time Caddy tries to fetch a certificate for a new subdomain, there’s a slight delay (a few seconds). Subsequent requests are instant because the certificate is cached. This isn’t a problem, but it caught me off guard the first time I added a new service.

Key Takeaways

This setup works well for internal services that I want to access securely without public exposure. It’s not suitable for public-facing websites because Tailscale certificates are only valid within your tailnet.

The main advantage is simplicity. No DNS providers, no ACME challenges, no certificate renewal scripts. Caddy and Tailscale handle everything automatically.

If you’re already using Tailscale and running services in Docker, this is the easiest way I’ve found to get HTTPS working without manual certificate management.

One limitation: you can’t use wildcard certificates with Tailscale. Each subdomain needs its own certificate, but Caddy handles this automatically as you add services.

I’ve been running this setup for over six months now with zero certificate-related issues. It just works.

Leave a Comment

Your email address will not be published. Required fields are marked *