Serve Arbitrary Customer Domains

I have a simple app that allows users to create websites, they can create as many as they want, and for each one select a subdomain. Those websites are being served on a different app, a web server written in Go. In front of it, I have a Traefik instance with a router for all subdomains of my domain my-domain.com. So when Traefik receives a request from site.my-domain.com it's sent to the web server, who does the lookup using the subdomain (site) and the URL to serve the specific page of the website.

So far, it works like a charm, but I'd like to allow users to serve the websites from their custom domains. How can this be achieved using Traefik? I'm using Docker to run the web server, and it's discovered automatically by the labels.

I tried adding a match-all (.+) router, but after that testing with a domain in Cloduflare that points with a CNAME to site.my-domain.com I get “The page isn’t redirecting properly” in my browser, and this on the logs.

2024-11-25T01:13:16.101262289Z 172.70.255.53 - - [25/Nov/2024:01:13:16 +0000] "GET / HTTP/1.1" 302 5 "-" "-" 13589 "http-custom-web-server@docker" "-" 0ms
2024-11-25T01:13:16.197325817Z 172.70.255.53 - - [25/Nov/2024:01:13:16 +0000] "GET / HTTP/1.1" 302 5 "-" "-" 13590 "http-custom-web-server@docker" "-" 0ms
2024-11-25T01:13:16.289336604Z 172.70.255.53 - - [25/Nov/2024:01:13:16 +0000] "GET / HTTP/1.1" 302 5 "-" "-" 13591 "http-custom-web-server@docker" "-" 0ms
2024-11-25T01:13:16.368442572Z 172.70.255.53 - - [25/Nov/2024:01:13:16 +0000] "GET / HTTP/1.1" 302 5 "-" "-" 13592 "http-custom-web-server@docker" "-" 0ms
2024-11-25T01:13:16.486507994Z 172.70.255.53 - - [25/Nov/2024:01:13:16 +0000] "GET / HTTP/1.1" 302 5 "-" "-" 13593 "http-custom-web-server@docker" "-" 0ms
2024-11-25T01:13:16.568014849Z 172.70.255.53 - - [25/Nov/2024:01:13:16 +0000] "GET / HTTP/1.1" 302 5 "-" "-" 13594 "http-custom-web-server@docker" "-" 0ms
2024-11-25T01:13:16.689138507Z 172.70.255.53 - - [25/Nov/2024:01:13:16 +0000] "GET / HTTP/1.1" 302 5 "-" "-" 13595 "http-custom-web-server@docker" "-" 0ms
2024-11-25T01:13:16.795994457Z 172.70.255.53 - - [25/Nov/2024:01:13:16 +0000] "GET / HTTP/1.1" 302 5 "-" "-" 13596 "http-custom-web-server@docker" "-" 0ms
2024-11-25T01:13:16.908056576Z 172.70.255.53 - - [25/Nov/2024:01:13:16 +0000] "GET / HTTP/1.1" 302 5 "-" "-" 13597 "http-custom-web-server@docker" "-" 0ms
2024-11-25T01:13:17.016533546Z 172.70.255.53 - - [25/Nov/2024:01:13:17 +0000] "GET / HTTP/1.1" 302 5 "-" "-" 13598 "http-custom-web-server@docker" "-" 0ms
2024-11-25T01:13:17.188462313Z 172.70.255.53 - - [25/Nov/2024:01:13:17 +0000] "GET / HTTP/1.1" 302 5 "-" "-" 13599 "http-custom-web-server@docker" "-" 0ms
2024-11-25T01:13:17.290696033Z 172.70.255.53 - - [25/Nov/2024:01:13:17 +0000] "GET / HTTP/1.1" 302 5 "-" "-" 13600 "http-custom-web-server@docker" "-" 0ms
2024-11-25T01:13:17.415569504Z 172.70.255.53 - - [25/Nov/2024:01:13:17 +0000] "GET / HTTP/1.1" 302 5 "-" "-" 13601 "http-custom-web-server@docker" "-" 0ms
2024-11-25T01:13:17.509496704Z 172.70.255.53 - - [25/Nov/2024:01:13:17 +0000] "GET / HTTP/1.1" 302 5 "-" "-" 13602 "http-custom-web-server@docker" "-" 0ms
2024-11-25T01:13:17.609654892Z 172.70.255.53 - - [25/Nov/2024:01:13:17 +0000] "GET / HTTP/1.1" 302 5 "-" "-" 13603 "http-custom-web-server@docker" "-" 0ms
2024-11-25T01:13:17.710262455Z 172.70.255.53 - - [25/Nov/2024:01:13:17 +0000] "GET / HTTP/1.1" 302 5 "-" "-" 13604 "http-custom-web-server@docker" "-" 0ms
2024-11-25T01:13:17.810025004Z 172.70.255.53 - - [25/Nov/2024:01:13:17 +0000] "GET / HTTP/1.1" 302 5 "-" "-" 13605 "http-custom-web-server@docker" "-" 0ms
2024-11-25T01:13:17.909763448Z 172.70.255.53 - - [25/Nov/2024:01:13:17 +0000] "GET / HTTP/1.1" 302 5 "-" "-" 13606 "http-custom-web-server@docker" "-" 0ms
2024-11-25T01:13:18.009393068Z 172.70.255.53 - - [25/Nov/2024:01:13:18 +0000] "GET / HTTP/1.1" 302 5 "-" "-" 13607 "http-custom-web-server@docker" "-" 0ms
2024-11-25T01:13:18.141593073Z 172.70.255.53 - - [25/Nov/2024:01:13:18 +0000] "GET / HTTP/1.1" 302 5 "-" "-" 13608 "http-custom-web-server@docker" "-" 0ms
2024-11-25T01:13:18.229113881Z 172.70.255.53 - - [25/Nov/2024:01:13:18 +0000] "GET / HTTP/1.1" 302 5 "-" "-" 13609 "http-custom-web-server@docker" "-" 0ms

Other concern I have is with HTTPS, how to make it work for any domain? Ideally, I don't want to reset Traefik and have downtime each time a user add/changes a custom domain.

Traefik configuration:

networks:
  coolify:
    external: true
services:
  traefik:
    container_name: coolify-proxy
    image: 'traefik:v3.1'
    restart: unless-stopped
    environment:
      - CLOUDFLARE_DNS_API_TOKEN=****
      - CF_ZONE_API_TOKEN=****
    extra_hosts:
      - 'host.docker.internal:host-gateway'
    networks:
      - coolify
    ports:
      - '80:80'
      - '443:443'
      - '443:443/udp'
      - '8080:8080'
    healthcheck:
      test: 'wget -qO- http://localhost:80/ping || exit 1'
      interval: 4s
      timeout: 2s
      retries: 5
    volumes:
      - '/var/run/docker.sock:/var/run/docker.sock:ro'
      - '/data/coolify/proxy:/traefik'
    command:
      - '--ping=true'
      - '--ping.entrypoint=http'
      - '--api.dashboard=true'
      - '--api.insecure=false'
      - '--accesslog=true'
      - '--entrypoints.http.address=:80'
      - '--entrypoints.https.address=:443'
      - '--entrypoints.http.http.encodequerysemicolons=true'
      - '--entryPoints.http.http2.maxConcurrentStreams=50'
      - '--entrypoints.https.http.encodequerysemicolons=true'
      - '--entryPoints.https.http2.maxConcurrentStreams=50'
      - '--entrypoints.https.http3'
      - '--providers.docker.exposedbydefault=false'
      - '--providers.file.directory=/traefik/dynamic/'
      - '--providers.file.watch=true'
      - '--certificatesresolvers.letsencrypt.acme.httpchallenge=true'
      - '--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=http'
      - '--certificatesresolvers.letsencrypt.acme.dnschallenge.provider=cloudflare'
      - '--certificatesresolvers.letsencrypt.acme.dnschallenge.delaybeforecheck=0'
      - '--certificatesresolvers.letsencrypt.acme.storage=/traefik/acme.json'
      - '--providers.docker=true'
    labels:
      - traefik.enable=true
      - traefik.http.routers.traefik.entrypoints=http
      - traefik.http.routers.traefik.service=api@internal
      - traefik.http.routers.traefik.tls.certresolver=letsencrypt
      - traefik.http.routers.traefik.tls.domains[0].main=my-domain.com
      - traefik.http.routers.traefik.tls.domains[0].sans=*.my-domain.com
      - traefik.http.services.traefik.loadbalancer.server.port=8080
      - traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https
      - traefik.http.middlewares.gzip.compress=true

      - coolify.managed=true
      - coolify.proxy=true

Labels for web server running in Docker container:

traefik.enable=true
traefik.http.routers.https-web-server.rule=HostRegexp(`^.+\.my-domain\.com$`)
traefik.http.routers.https-web-server.entryPoints=https
traefik.http.routers.https-web-server.middlewares=gzip
traefik.http.routers.https-web-server.service=web-server-service
traefik.http.services.web-server-service.loadbalancer.server.port=3001
traefik.http.routers.https-web-server.tls=true
traefik.http.routers.https-web-server.tls.certresolver=letsencrypt
traefik.http.routers.http-web-server.rule=HostRegexp(`^.+\.my-domain\.com$`)
traefik.http.routers.http-web-server.entryPoints=http
traefik.http.routers.http-web-server.middlewares=redirect-to-https

# New configuration for any domain, this isn't working properly
# HTTPS router for any domain
traefik.http.routers.https-custom-web-server.rule=HostRegexp(`^.+$`)
traefik.http.routers.https-custom-web-server.entryPoints=https
traefik.http.routers.https-custom-web-server.middlewares=gzip
traefik.http.routers.https-custom-web-server.service=web-server-service
traefik.http.routers.https-custom-web-server.tls=true
traefik.http.routers.https-custom-web-server.tls.certresolver=letsencrypt

# HTTP router for any domain (redirect to HTTPS)
traefik.http.routers.http-custom-web-server.rule=HostRegexp(`^.+$`)
traefik.http.routers.http-custom-web-server.entryPoints=http
traefik.http.routers.http-custom-web-server.middlewares=redirect-to-https

Thanks in advance for any reply!

With pure RegEx you can’t get TLS certs. You need to name the domains, either in Host() or TLS config.

To add new domains, you need to add a config for every single domain. To do this without interruption, you need to update the Traefik config while running. Either you watch a dynamic config file or load a config via providers.http every couple of seconds.

I would probably add a separate router for every domain, not have a single router and add all domains.

Note that the customer always needs to create a CNAME for the domain first, you should very that, if possible without Traefik. Then add config and use httpChallenge or tlsChallenge for the cert, but have a wait page of at least 30 sec before forwarding the customer to the new domain.

Alternatively you use an external tool like certbot to create the TLS cert, you can then probably easier read the result instead of parsing live Traefik debug log for acme LetsEncrypt. The generated TLS can be saved as plain file and loaded in dynamic config, or you can inline it into dynamic config.

1 Like

Thank you!!! Your comment pointed me in the right direction. I created an HTTP service that serves as a provider for Traefik and creates the dynamic config from the database records. As you suggested using a router for each domain (actually 2, one for HTTP and other for HTTPS, HTTPS only didn't work, idkw). The certs are generated automatically by Traefik, I'm fine with that since I think I can check the domain status and notify the customer once it is ready by Traefik.

Now I'm wondering how to scale this to other regions to improve latency. Should I create new certificates on the new regions, or try to use the ones already generated by one instance? Or is this a good time to move certificate generation/storage out of Traefik? Because as fair as I can see, Traefik is storing the certificates in a JSON file in the host.

This topic was automatically closed 3 days after the last reply. New replies are no longer allowed.