Today I am going to talk about how I implemented custom domains support in DynaSite. See the previous post where I talk about my plans to relaunch DynaBlogger as DynaSite, with new features including an AI-enhanced page builder.
Since I launched DynaBlogger, one of the most common questions I get asked is "how did you implement custom domains?"
The main reason I get asked this is TLS/SSL and certificates. To implement custom domains in your SaaS you need to somehow provision certificates for these domains.
Also the way you provision certificates must be able to scale regardless of how many customers (and thus how many custom domains) your SaaS needs to handle. This is a problem even with popular PaaS like Heroku because there's a limit with the number of custom domains that they can support for a single app. In some cases you can contact support to raise these limits but not sure up to what extent.
An alternative is to build something yourself based on Let's Encrypt to be able to offer certificates for free, especially if you manage the apps on your servers, but it can get complicated as your infrastructure grows and you need to manage and use certificates with many servers and load balancers.
For DynaBlogger, the platform was hosted on a Kubernetes cluster in Hetzner Cloud (created with my open source tool https://github.com/vitobotta/hetzner-k3s), therefore the natural choice for me to manage free Let's Encrypt certificates was cert-manager.
The way the basic functionality works is quite simple:
- user adds a custom domain to their blog
- the app creates an ingress resource in Kuberrnetes for that domain with support for unencrypted traffic on port 80
- the app exposes a special, unique and difficult to guess path at http://domain.com/verification/<some long UUID>
- the app polls every few seconds that path and checks the response. If the response contains a matching key, then it flags the domain as verified because it means that the domain is correctly pointing to our app. The domain remains in pending verification state in the UI until this condition is met
- once the domain is verified, the ingress resource is "upgraded" with support for encrypted connections, meaning that it adds to the ingress resource the required annotation to trigger a certificate issuance request to Let's Encrypt via cert-manager
- once the certificate is issued successfully, the domain is marked as ready and the app can start serving the user's content from their custom domain
- when a custom domain is removed by the user, the relevant ingress resource is also deleted
You might ask, why perform a verification ourselves if Let's Encrypt already does this? The reason is that there are some rate limits that take into account also verification failures. So before even letting cert-manger request a certificate, I make sure that the domain is in fact pointing to my app.
I might also add that to make the verification more robust, and ensure that the user who is currently trying to use the domain owns it, you might want to use some sort of DNS based verification instead, which can help prevent a form of attacks called "subdomain takeover".
This process is quite simple and doesn't require much coding and can scale a lot with a Kubernetes cluster regardless of how many nodes etc you have; once you reach the limit of how many virtual hosts the ingress controller can handle, you can scale with multiple ingress controller, and so on.
The next question I get asked often is, "how do I do implement this stuff if I don't use Kubernetes?"
These past few evenings I have started to work on the DynaBlogger relaunch as DynaSite, but until the actual launch I won't be setting up a Kubernetes cluster and will use a single server to cut costs until proper scaling is required.
At the moment I am using Dokku on this single server to host the app, and I had to figure out how to handle custom domains while I keep this simpler setup. I evaluated a few options, and then finally settled on Cloudflare for SaaS since I already use Cloudflare and it's a brilliantly simple solution. I am probably going to keep this even when I launch since I really like it as a solution and doesn't require my users to use Cloudflare for their domains. 100 hostnames are free and after that it's just $0.10 per hostname.
Cloudflare for SaaS
Once the domain is ready, you need to configure a DNS record pointing to your SaaS. Cloudflare will forward any traffic for custom hostnames to the hostname you configure in this record. Make sure the proxy is enabled for this record (orange cloud).
Next, head to
and enable the feature, then add a "fallback origin":
Once it's active, you can start adding custom domains for your users. For simplicity, I use the HTTP verification method:
Give it a few moments, and then the hostname will appear as ready:
To complete the configuration, we need to remove the default site virtual host configured in Nginx:
rm /etc/nginx/sites-enabled/default dokku nginx:stop dokku nginx:start
And create a new virtual host that points to your SaaS app, which will be a "catch-all" for domains not directly configured here. Create this file at /etc/nginx/conf.d/00-default-vhost.conf:
Of course replace "dynasite" with the name of your app, and "3000" with its correct container port.
Finally, reload nginx:
sudo nginx -t sudo nginx -s reload
Update: see the next post on how I built a landing page and waitlist for DynaSite, to capture email addresses of people who might be interested in the product while I build it.