I’m building a multi-tenant platform and, I’m trying to setup custom domain connection such that my users can connect their tenants (example user1.myplatform.com) to their own custom domains like user1.com.
To do so, I’m creating a traefik configuration file for each verified custom domain in the dynamic config folder (/data/coolify/proxy/dynamic) and that file looks like this
http:
routers:
manual-test-router:
rule: "Host(`user1.com`) || Host(`www.user1.com`)"
entryPoints:
- websecure
service: <SERVICE-NAME>
tls:
certResolver: letsencrypt
The DNS records are verified to be set correctly. And with this configuration, when i visit the user1.com, I just get a page load error with this error code: net::ERR_CERT_AUTHORITY_INVALID.
For what it’s worth, the backend was hosted on a hetzner vps, Using Coolify dashboard for deployment management et al.
How did you define certResolver: letsencrypt in static config?
Hi, thanks for the response.
Here’s the Go code that creates the file for every custom domain:
func (s *DomainService) CreateCustomDomainTraefikConfig(websiteConfigID string, subDomain string, domain string) (bool, error) {
const traefikTemplate = `
http:
routers:
router-{{.ID}}:
rule: "Host('{{.Domain}}') || Host('www.{{.Domain}}')"
entryPoints:
- {{.EntryPoint}}
service: {{.ServiceName}}
tls:
certResolver: {{.CertResolver}}
`
labelsServiceName := os.Getenv("TRAEFIK_LABELS_SERVICE_NAME")
if labelsServiceName == "" {
return false, fmt.Errorf("TRAEFIK_LABELS_SERVICE_NAME environment variable is not set.")
}
certResolver := "letsencrypt"
entryPoint := "websecure"
config := DomainConfig{
ID: websiteConfigID,
Domain: domain,
ServiceName: labelsServiceName,
CertResolver: certResolver,
EntryPoint: entryPoint,
}
fileName := fmt.Sprintf("%s.yaml", config.ID)
fullPath := filepath.Join(dynamicBasePath, fileName)
if err := os.MkdirAll(dynamicBasePath, 0755); err != nil {
return false, fmt.Errorf("failed to ensure dynamic config directory exists: %w", err)
}
// Generate YAML
tmpl, err := template.New("traefik").Parse(traefikTemplate)
if err != nil {
return false, err
}
file, err := os.Create(fullPath)
if err != nil {
return false, err
}
defer file.Close()
err = tmpl.Execute(file, config)
if err != nil {
return false, err
}
return true, nil
}
Has anyone solved this problem? I have the same problem as @dondxniel. I will add some of the things I have tried, as it might help us all to solve this issue:
Just as @dondxniel my customers have their own domains (example: www.clientdomain.com) that they want it to point to their subdomain on my service/server (client.myservice.com). In their domain registrars I had them add a CNAME to point www to their subdomain client.myservice.com (I even told them to add an A record pointing to the actual IP of my server, just in case).
Browsing to www.clientdomain.com does go to the right place, but is served with Traefik’s self signed certificate, thus showing the browser’s Security Warning. These are the logs I see in my Traefik’s docker container:
2026-01-09T20:05:08-04:00 ERR Unable to obtain ACME certificate for domains error="unable to generate a certificate for the domains [www.clientdomain.com]: error: one or more domains had a problem:\n[www.clientdomain.com] [www.clientdomain.com] acme: error presenting token: cloudflare: failed to find zone clientdomain.com.: zone could not be found\n" ACME CA=``https://acme-v02.api.letsencrypt.org/directory`` acmeCA=``https://acme-v02.api.letsencrypt.org/directory`` domains=["client.myservice.com","www.clientdomain.com"] providerName=letencrypt.acme routerName=client@docker rule="Host(client.myservice.com) || Host(www.clientdomain.com)"
It would seem that is trying to find the my client’s domain as “zone” in my CloudFlare and does not find it. But obviously it will never be there because my CloudFlare account doesn’t own that domain.
How can we solve this? Thanks!!!
I fixed it. This is what I did:
1 - Make sure that your customer added an A RECORD pointing to the IP Address of your server.
2 - Add a new ACME Certificate Resolver (based on HTTP-01 instead of DNS/CloudFlare, since the custom domains are NOT under your control).
Add the new resolver under the “certificatesResolvers“ section in traefik.yml file (keep you cloudflare/dns one intact… you just need to add a new one):
certificatesResolvers:
letencrypt:
acme:
email: youremail@gmail.com
storage: /certs/acme.json
caServer: https://acme-v02.api.letsencrypt.org/directory
dnsChallenge:
provider: cloudflare
delayBeforeCheck: 10
http:
acme:
email: youremail@gmail.com
storage: /certs/acme-http.json
httpChallenge:
entryPoint: web
3 - If you are using docker containers (I am), for the underlying services, you need to add new label lines under the “labels” section (these are new lines, keep the old ones intact).
labels:
# EXISTING LINES
- "traefik.enable=true"
- "traefik.http.routers.${CUSTOMER_NAME}.rule=Host(${CUSTOMER_NAME}.yourservice.com)"
- "traefik.http.services.${CUSTOMER_NAME}.loadbalancer.server.port=8069"
- "traefik.http.routers.${CUSTOMER_NAME}.entrypoints=websecure"
- "traefik.http.routers.${CUSTOMER_NAME}.tls=true"
- "traefik.http.routers.${CUSTOMER_NAME}.tls.certresolver=letencrypt"
\# NEW LINES TO ADD ALL THE CUSTOM DOMAINS TO THE SAME CONTAINER
- "traefik.http.routers.${CUSTOMER_NAME}-custom-0.rule=Host(\`www.custom1.com\`)"
- "traefik.http.routers.${CUSTOMER_NAME}-custom-0.tls.certresolver=http"
- "traefik.http.routers.${CUSTOMER_NAME}-custom-1.rule=Host(\`custom1.com\`)"
- "traefik.http.routers.${CUSTOMER_NAME}-custom-1.tls.certresolver=http"
NOTES
- Note that each custom domain for the customer ends in -0, -1, etc. This is needed to avoid router/rules override. If you are adding only 1 custom domain, then this is not needed because there rule name is still unique.
Hope this helps.
Awesome. Thanks for coming back with the fix.
I do have a few questions though.
- Where do you find the traefik.yml file? I’m a bit new to managing deployments. And I’m using coolify.
- I am using docker containers, and based on what you say here about adding the new labels, it seems you’re adding them manually. Please correct me if i’m mistaken. If that’s the case, do you have any idea how I can configure mine to be added automatically? Or if I can have some sort of wildcard.
Hello @dondxniel
I don’t use coolify, but I just googled and found this:
1 - About the traefik.yml file:
In that article I can see that in the config file you use for coolify (which seems to just be a docker compose file) you can define the HTTP certificate resolvers (look at the lines that start with: --certificatesresolvers.letsencrypt.acme.). In that example the uncommented certificateresolver lines are using Hetzner DNS. The commented lines are part of what you need to add an HTTP resolver (for your customer’s custom domains). If we translate the certificate resolver for HTTP i shared before (unfortunately all the indentation is removed in here):
http:
acme:
email:youremail@gmail.com
storage: /certs/acme-http.json
httpChallenge:
entryPoint: web
to use it directly in the docker file, I would say it should look something like this:
- '--certificatesresolvers.letsencrypt.acme.httpchallenge=true'
- '--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=http'
- '--certificatesresolvers.letsencrypt.acme.storage=/traefik/acme-http.json'
NOTE: in my case the entrypoint is named web. In the example is named http (second line).
You should leave both enabled: the CloudFlare one (if you are using DNS resolving and your domain is in CLoudFlare) for YOUR domain and add the HTTP one to be able to resolve the ones for your customers.
Remember that your customer must add an A record pointing to your IP. Let’s Encrypt needs that to know and issue the certificate.
2 - About the docker labels for traefik auto-discovery:
In my case, I modify the docker compose file of that particular customer’s instance through a script (not all customers require custom domains in our case). I am using Cetmix Tower (my company works with Odoo, so Cetmix was a good fit for me) to run a command whenever I need to add/remove domains to my client’s instances. When/where are you deciding that your customer requires a custom domain? That’s when you need to modify the docker compose file of your customer’s deployment (whenever I add/remove labels to the docker compose file of my customer’s instance, I do a compose down / up to restart it to make sure traefik detects the changes in the labels and re-requests the certificate to Let’s Encrypt.
Hope this helps!
Thank you very much, this was very helpful.
I’ve fixed the issue.