Add Let’s Encrypt SSL Certificates on Private Domains with Dokku

If you use Dokku, you’re probably already familiar with the Let’s Encrypt plugin to enable HTTPS on your apps with a single command.

The plugin uses certbot with the HTTP-01 challenge that requires that the app be accessible on the Internet. This can’t work on apps that live in a private network.

At Bixoto, we use Tailscale and Headscale to manage our VPN. All machines have an address in a private IP range. We have a domain dedicated to VPN usage that we’ll call bx.vpn in the rest of the post; we bought it on OVH and manage its DNS with OVH’s servers. It has one subdomain per machine/app that points to its private address.

The DNS zone looks like this:

# IPv4
itchy      3600 IN A
scratchy   3600 IN A
snowball   3600 IN A

# IPv6
itchy      3600 IN AAAA   fd7a:xxxx:xxxx::1
scratchy   3600 IN AAAA   fd7a:xxxx:xxxx::2
snowball   3600 IN AAAA   fd7a:xxxx:xxxx::3

That way, anyone can resolve itchy.bx.vpn to, but since it’s a private IP you have to be in the private network to access the machine it points to.

Dokku apps are configured to listen only on the private interfaces:

# Bind to the private addresses of the server Dokku runs on
dokku nginx:set $APP_NAME bind-address-ipv4
dokku nginx:set $APP_NAME bind-address-ipv6 fd7a:xxxx:xxxx::3
dokku proxy:build-config $APP_NAME

That way, even if you know the public IP address of the server that hosts Dokku, you can’t access the apps. That’s good for our security, but if we want to have HTTPS on our private apps, we can’t use Let’s Encrypt as-is because it needs to access our apps over HTTP.

The solution is to use the DNS-01 challenge, whose support was added in the Dokku Let’s Encrypt plugin in January, 2023 for wildcard domains. The official docs only talk about wildcard domains, but this challenge can also be used for private addresses.

The DNS-01 is a DNS equivalent of the HTTP-01 challenge: instead of asserting that you own the server the domains points to, it checks that you have access to the DNS zone (= you own the domain). To do so, it needs access to your DNS provider to add a temporary subdomain that is then checked by Let’s Encrypt’s server.

This is the setup for OVH, but other providers are supported.

Go on OVH to create an API key. Fill the name and description, set the validity to “Unlimited”, and in “rights”, make sure you allow POST /domain/zone/* and DELETE domain/zone/*.

Then, copy your credentials and set them in Dokku:

dokku letsencrypt:set --global dns-provider-OVH_APPLICATION_KEY "..."
dokku letsencrypt:set --global dns-provider-OVH_APPLICATION_SECRET "..."
dokku letsencrypt:set --global dns-provider-OVH_CONSUMER_KEY "..."
dokku letsencrypt:set --global dns-provider-OVH_ENDPOINT ovh-eu  # or ovh-ca

Note: these variables can also be set at the application level.

Now, to make an application use DNS-01 instead of HTTP-01, set the dns-provider property:

dokku letsencrypt:set $APP_NAME dns-provider ovh

If you did everything right, you should be able to run dokku letsencrypt:enable $APP_NAME; Let’s Encrypt will use the DNS-01 challenge and you’ll get HTTPS on your app even if it’s not accessible from the public Internet.