Using Coolify to deploy applications with wildcard SSL on dynamic subdomains

By Hugo LassiègeOct 20, 20255 min read

In the last post we saw how to deploy a multi-tenant application, built with Nuxt, on Coolify in a way that allows us to dynamically manage *.myapp.com subdomains with a single application.

If you haven't read that post, I suggest reading it first, especially to refresh your memory on the terminology, because I'm not going to cover it again here.

However, I had somewhat overlooked a detail that cost me a few extra days of work: SSL

It's all well and good to be able to dynamically manage subdomains, but if the SSL certificates are all invalid, it doesn't inspire much confidence.

So now we're going to see:

  • how to configure a certificate resolver that does DNS challenge
  • how to use it to generate a wildcard SSL certificate for all subdomains
  • how to then give users the ability to use their own custom domain with their subdomain

A certificate resolver

I'll be honest, I wouldn't have bet on writing an article about this a few weeks ago because it's far from my area of expertise.

But hey, I pulled my hair out for a few days, and also because I had trouble understanding the docs, so I might as well share it with others.

Anyway, let's get back to the basic problem: we want an SSL certificate on each subdomain we're going to use dynamically, so user1.myapp.com, user2.myapp.com, etc...

Normally in Coolify, we deploy an application on a single domain (or a well-defined list anyway).

At deployment time, we ask Let's Encrypt to generate a certificate. It will perform an HTTP challenge to verify that we indeed own the domain in question. It will verify this by checking a file we'll have placed on the server, for example http://user1.myapp.com/.well-known/acme-challenge/[token]

It's transparent for us with Coolify and Traefik, everything is handled without any issue.

The problem is that with dynamic subdomains this method doesn't work very well.

Either we list them in Coolify one by one with each new addition. It's a possibility, but I haven't found how to automate this. In any case, it involves a lot of HTTP validations to do, to renew regularly.

Or we request a wildcard certificate. It's an elegant solution, we'll only request a single validation and save ourselves the waiting time for certificate availability. Once it's available, it works for all subdomains.

But with a wildcard certificate, HTTP challenge validation no longer works. Why? Because Let's Encrypt can't verify a file on a domain that doesn't exist yet. So we need to prove we control the domain differently: by directly modifying the DNS.

That's DNS challenge. Instead of verifying a file on the server, Let's Encrypt will verify a TXT record in a DNS zone (like _acme-challenge.myapp.com).

And for that, Traefik needs to be able to control the DNS server to automatically add a record to our DNS.

Fortunately, Traefik knows how to do this using the public APIs of providers. Well, as long as that provider is in the list of providers managed by Lego

Bad surprise, at the time I was working on this, my provider "hostinger" wasn't managed by Traefik. So I had to migrate to bunny.net. It's a European provider where I also manage my CDN and storage. (since then, hostinger has become available in Traefik)

Configuring Traefik to add a certresolver

To configure Traefik, you need to select your server in Coolify:

side menu
side menu

Then go to Proxy

proxy menu
proxy menu

And then it's possible to add commands that will allow us to configure an additional cert resolver:

      - '--certificatesresolvers.letsencrypt-dns.acme.dnschallenge.provider=bunny'
      - '--certificatesresolvers.letsencrypt-dns.acme.dnschallenge.delaybeforecheck=0'
      - '--certificatesresolvers.letsencrypt-dns.acme.storage=/traefik/acme.json'     

I also added labels

      - traefik.http.routers.traefik.tls.domains[0].main=writizzy.io
      - traefik.http.routers.traefik.tls.domains[0].sans=*.writizzy.io      

Be careful, you also need to add the bunny API key in environment:

    environment:
      - BUNNY_API_KEY=mykey

Just restart Traefik, check the logs and this step is complete.

Using the new traefik resolver

That's all well and good, but now we need to tell our application to use this resolver. To do this, we go back to the configuration of the application in question, in the container labels and we go from this:

traefik.http.routers.https-0-ow8g70707sockck8sgo8kc55g.tls.certresolver=letsencrypt

to this:

traefik.http.routers.https-0-ow8g70707sockck8sgo8kc55g.tls.certresolver=letsencrypt-dns

ow8g70707sockck8sgo8kc55g is a random name generated by Coolify, it's certainly not the same for you...

Managing custom domains

Let's imagine a user wants to use theirdomain.com instead of myuser.myapp.com. This is what we call a custom domain and it's used very frequently in many SaaS products you use regularly.

Generally for this, you need to configure a CNAME record that points from theirdomain.com to myuser.myapp.com

But obviously Traefik doesn't know what to do with a request to theirdomain.com, it's a domain unknown to it so the result will be a magnificent error message "no available server".

For these domains, we're going to ask our multi-tenant container to add a custom router that will catch all unknown traffic:

traefik.http.routers.https-custom.rule=HostRegexp(`^.+$`)
traefik.http.routers.https-custom.entryPoints=https
traefik.http.routers.https-custom.service=https-0-zgwokkcwwcwgcc4gck440o88
traefik.http.routers.https-custom.tls.certresolver=letsencrypt
traefik.http.routers.https-custom.tls=true
traefik.http.routers.https-custom.priority=1

The regexp allows us to say that all received traffic should go through this custom router, with a priority of 1 (so very low) to avoid conflicting with all other containers deployed on the same Traefik. Here we'll note that we're switching back to the letsencrypt resolver with HTTP challenge type. Why? Because for custom domains, we know the exact domain at the time of the certificate request (the user gives it to us), so no need for a wildcard and HTTP challenge becomes possible again.

And normally, that's it.

But I can tell you it took me longer to understand all this than to write it...


Share this:

Written by Hugo Lassiège

Software Engineer with more than 20 years of experience. I love to share about technologies and startups

Copyright © 2025
 Eventuallymaking
  Powered by Bloggrify