How we use Tailscale and Caddy to develop over HTTPS

Tailscale recently launched a beta version of auto-HTTPS support, and I wanted to quickly document how we are using it on our team here at Shareup. We ❤️ Tailscale and use it everyday, and this auto-HTTPS support has made sharing in-development apps and services with all our devices and between our development machines super easy.

The way Tailscale’s HTTPS feature works is they provide you a custom subdomain of ts.net which will look similar to [machine-name].[random-words].ts.net. Since Tailscale owns and operates ts.net, they can provision real SSL certificates for subdomains using Lets Encrypt.

You can find your machine on the Tailscale dashboard, and it will show your custom subdomain there in the machine info.

Screenshot of the Tailscale Dashboard’s machine information showing where the machine name can be found, which is next to a label ‘Domain’

One can also find the same info in a local terminal or shell by running tailscale status --peers=false --json and looking for the DNSName property in the Self object or looking in the CertDomains array. We use jq to find it: tailscale status --peers=false --json | jq -r '.CertDomains[]'

We use Caddy to proxy HTTPS traffic to our HTTP services running on localhost. We like Caddy because it already knows about Tailscale and will automatically pull down your certificates to set up HTTPS endpoints. You can install Caddy for your platform (we use Homebrew).

The easiest way to get going with Caddy is to make a Caddyfile.

To make things more predictable, we have a convention for translating an HTTPS port to an HTTP port: we add 10,000 to the HTTP port number. For example: if we have a local service running bound to localhost:3000 then we expect to be able to visit that using tailscale’s HTTPS url at
[machine-name].[random-words].ts.net:13000.

Here is an example Caddyfile that would configure the above port proxying:

[machine-name].[random-words].ts.net:13000 {
    reverse_proxy localhost:3000
}

You can then boot caddy with that configuration in a terminal / shell:

caddy run --config Caddyfile

(Depending on how you install Caddy, it may already be running and have a default Caddyfile. If you have issues running caddy then check to make sure it’s not already running or edit its default Caddyfile instead of making a new one.)

We run a few services locally, so we need to list them all in a config to proxy 13000, 13001, 13002, … to 3000, 3001, 3002, …

We have a script which auto-generates this Caddyfile and writes out a .env.overrides file to override our environment variables while the caddy proxy is running so our different services are able to find each other over HTTPS. Something like:

export WEBSOCKET_URL=wss://[machine-name].[random-words].ts.net:3003/
export WEB_ORIGIN=https://[machine-name].[random-words].ts.net:3001/
export METADATA_SERVICE_URL=https://[machine-name].[random-words].ts.net:3002/

This setup has greatly improved testing on all of our devices 🙌 We also are more likely to share a quick link to see an in-development service or web app with each other, since we know it will load over HTTPS without any additional setup, having to share keys, etc.

If you have Tailscale installed, and have the HTTPS feature turned on, you can get Caddy working for you as well with a similar configuration file. 🚀