Blog
Dec 17, 2025

Managing Custom Domains and Dynamic SSL with Coolify and Traefik

Hugo

I recently published an article on managing SSL certificates for a multi-tenant application on Coolify.

The title might sound intimidating, but basically it lets an application handle tons of subdomains (over HTTPS) dynamically for its users. For example:

  • https://hugo.writizzy.com
  • https://thomas-sanlis.writizzy.com

That's a solid foundation, but users often want more: custom domains. The ability to use their own address, like:

  • https://eventuallymaking.io
  • https://thomas-sanlis.com

So let's dive into how to manage custom domains for a multi-tenant app with dynamic SSL certificates.

(I feel like I'm going for the world record in longest blog post title!)

Custom Domains

The first step for a custom domain is routing traffic from that (sub)domain to your application (Writizzy in my case).

It all comes down to DNS records on the user's side. Only they can configure their domain to point to your application's subdomain.
They have several options: CNAME, ALIAS, or A records.

1. CNAME (the simplest)

A CNAME is an alias for another domain. It basically says that www.eventuallymaking.io is an alias for hugo.writizzy.com. All traffic trying to resolve the www address gets automatically forwarded to your application domain.

Major limitation: You can't use a CNAME for a root domain (e.g., eventuallymaking.io without the www). Adding a CNAME on an apex domain would conflict with other records (A, MX, TXT, etc.).

2. ALIAS

Some DNS providers offer ALIAS records (or CNAME flattening). It's essentially a CNAME that can coexist with other records on a root domain. Great option if the user's provider supports it (OVH doesn't, for instance).

3. A Record

Here, the user directly enters Writizzy's server IP address.

Warning: This approach is risky. If you change servers (and therefore IPs), all your users' custom domains break until they update their config. To use this method safely, you need a floating IP that can be reassigned to your new server or web frontend.

Alright, that's a good start, but if we stop here, things won't work well—the site won't serve over HTTPS.

HTTPS on Custom Domains

In my previous post, I showed how Traefik could route all traffic hitting *.writizzy.com subdomains to the same application using these lines:

yaml
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

Since the regex catches everything, you might think SSL would just follow along. Unfortunately, no. Traefik knows it needs to handle a wildcard certificate for *.writizzy.io, but it has no clue about the external domains it'll need to serve.

We need to help it by dynamically providing the list of custom domains.

Our constraints:

  • Obviously, no application restart
  • We want to drive it programmatically

Dynamic Traefik Configuration with File Providers

This is where Traefik's File Providers come in.

In Coolify, this feature is enabled by default via dynamic configurations. Under the hood, Traefik watches a specific directory:

bash
- '--providers.file.directory=/traefik/dynamic/'
- '--providers.file.watch=true'

Drop a .yml file defining the rule for a new domain, and Traefik picks it up hot and triggers the HTTP challenge with Let's Encrypt to get the SSL certificate.

Example dynamic configuration:

yaml
http:
  routers:
    eventuallymaking-https:
      rule: "Host(`eventuallymaking.io`)"
      entryPoints:
        - https
      service: https-0-writizzy
      tls:
        certResolver: letsencrypt
      priority: 10
    eventuallymaking-http:
      rule: "Host(`eventuallymaking.io`)"
      entryPoints:
        - http
      middlewares:
        - redirect-to-https@docker
      service: https-0-writizzy
      priority: 10

So with this approach, we've solved our first constraint: no restart needed to configure a new custom domain.

Now let's tackle the second one and make it programmatic.

Automation via the Application

The idea is straightforward: the application creates these files directly in the directory Traefik watches.

  1. Coolify configuration: Go to Configuration -> Persistent Storage and add a Directory mount to make the /traefik/dynamic/ directory visible to your application container.

directory mount
directory mount

  1. Code (Kotlin in my case): The application generates a file based on the template above as soon as a user configures their domain.

Important note on timing: If you create the Traefik config before the user has pointed their domain to your IP, the SSL challenge will fail. Traefik will automatically retry (with backoff), but it can take a while. Ideally, validate the DNS pointing (via a background job) before generating the file.

Important note on security: The generated file contains user input (the domain name). It's crucial to sanitize and validate this data to prevent a malicious user from injecting arbitrary Traefik directives into your configuration.

Wrapping Up

This post wraps up what I hope has been a useful series for SaaS builders running multi-tenant apps on Coolify. We've covered:

  1. Tenant management (Nuxt) and basic Traefik configuration
  2. SSL management with dynamic subdomains
  3. Custom domains with SSL (this post)

You might wonder if this solution scales with tens of thousands of sites all using custom domains—specifically whether Traefik can handle monitoring that many files in a directory.

I don't have the answer yet. Writizzy is nowhere near that scale for now. There might be other solutions, like using Caddy instead of Traefik in Coolify, but it's way too early to explore that.

Enjoyed this article?

Subscribe to receive the latest articles directly in your inbox.

0 Comments

No comments yet. Be the first to comment!