Do you want to let your users connect their own domains or subdomains to your app, like Shopify lets you put your own domain on a shop?
This guide will teach you how to build that feature yourself.
How I know this: I've been building web services for over a decade, and I've built approximated.app for this exact purpose, which reliably serves 300k+ domains every month.
This guide is going to be pretty long. If you'd rather pay $20/month to have this all done for you, head over to approximated.app. If you'd prefer to build it yourself, then read on!
Lets start with a few example web apps:
Each of those can benefit a lot by allowing users to connect custom domains:
cartersblog.com instead of wehostblogs.com/cartersblog for the SEO juice.leshopdecarter.com instead of ecommercesite.com/leshopdecarter for the branding AND the SEO juice.invoices.theirapp.com instead of paymentprovider.com/invoices/theirapp for the branding but also because it feels less professional to send customers to someone else's domain.There's real value to adding custom domains, and I would even go as far as to say that pretty much every web app/service can benefit from this in some way.
Even if your app doesn't provide services that your end users will have public facing, say a project management app, they still like it if they can point a domain/subdomain at it and brand it as their own.
Hot tip: especially for larger customers, custom domains are a great up-sell feature to help entice them towards business or enterprise plans. Having things under their own branding matters a lot to them and they're willing to pay for it.
If you have a web app, you probably already know most of this, but some people use different terms and the language around this can get confusing quickly. Here are the important terms I'll be using:
example.com or indiehackers.com. It doesn't include the https:// before it or the paths after it like /some/pathsomething.example.com, something is the subdomain. Most of the time when people refer to a subdomain, they're talking about the entire thing though. Fun fact: subdomains technically are a domain as well.<any-subdomain>.yoursite.com with one certificate. Very useful if you have a lot of subdomains.There's a few different things you'll need at a minimum, depending on the level of sophistication you want/need for your use case.
I'll explain how to use most of these later, but the pieces are:
If that seems like a lot and at this point you'd like to just have it done for you, approximated.app will have you covered. If you'd still like to build this yourself, lets continue on.
At a high level, the flow of this whole system works like this:
customdomain.comFirst off, you'll need to decide how you want to run Caddy:
It's a reverse proxy, so you could just plunk it down next to your app on whatever server you're running and route all of the app traffic through it. If you're running a backend/fullstack framework like Laravel, Rails, Phoenix, etc. this might be the easiest method for you. If you already have a reverse proxy running, I'd recommend replacing it instead of stacking them (routing through one, then the other) unless you have a very good reason. Otherwise it just complicates things.
This increases the number of network hops but usually has a very minimal impact on latency or response time, provided it's somewhat near your app server. So why would you want to do this? Mainly when you don't have (or maybe want) full control of your backend. People using platforms like Vercel, Render, Firebase, etc. where you don't have the option of installing something like Caddy on the server side. Or alternatively, if aren't sure where all you might need to point to in the future. Say, if you offer a self hosted product but want to offer some kind of integrated custom domains service to whatever server they might be running.
There are plenty of cases where you might want to run a whole cluster of Caddy instances, but the main scenario is:
Now imagine a request from a user in Canada is sent to one of the custom domains connected to your app and you only have one Caddy instance in, let's say, France. That request is going to have to travel this route:
Not great, right? To solve this, you can run multiple instances of Caddy in different regions near your users and app servers, so that the route is always short. This can get pretty complicated though, because you'll need to keep their configurations up to date on every instance, and share SSL certs that get generated between them so you don't A) confuse browsers, and B) generate an SSL cert for the same domain multiple times and run into rate limits with certificate authorities that issue the certs.
This is a whole topic on it's own, so I won't go any deeper into it in this guide, but if you want really easy clustering, approximated.app will do it for you with a click.
Once you've decided where you're going to host Caddy, you'll need to actually go and get it on the server. There's a few ways you can do this, and the Caddy docs here make it pretty easy: https://caddyserver.com/docs/install. Generally I'd recommend the daemon/service official packages if you can. That way it'll be configured to restart if it ever crashes and on system startup. Plus it's usually easier to manage starting/stopping and logging that way.
After you have that installed (or the binary or a docker image with it), you can start it up. It'll have a very minimal config that it runs off of at the moment and do basically nothing until we configure it some more.
There's two main ways to configure Caddy - a config file it reads on startup, or it's admin API endpoint that defaults to localhost:2019 (docs: https://caddyserver.com/docs/api). I recommend, for a single Caddy instance, to use the admin API endpoint.
Warning: tripping point:
If you're using one of the official packages like the Debian one then it actually installs two services, one called caddy which starts up with a config file argument, and one called caddy-api which doesn't. Both services will auto-save your config to disk when it receives updates over the admin API, but the regular caddy service will overwrite it on restart with the original config file. To avoid this, run systemctl disable caddy then systemctl enable caddy-api (or your OS equivalent) to switch to caddy-api instead.
Regardless of how you're updating Caddy's config, you should make sure you're always able to recreate that config from scratch elswhere, like your app. Don't rely on Caddy's config autosave feature alone. At the very least back it up regularly.
Now that you know how you can set the configuration in Caddy, you'll need to decide on a strategy for updating the configuration itself when custom domains are created/updated/removed in your app. They all have their own trade-offs, which one works best for you will depend on your situation.
A few options are:
However you end up doing this, be sure that you don't accidentally open up the admin API to the public internet or something similar. Otherwise anyone who finds it (and their are bots looking for it) can control your Caddy instance, intercept requests, and generally do whatever they want with it. By default, Caddy's admin API is localhost only and has some sane defaults to prevent that, but I've also seen people make it public without considering the risk.
When you're ready to start updating the config with custom domains, you'll want to use Caddy's HTTP routes feature, with a reverse proxy handler.
Each route can be matched to almost anything in a request using the match fields. In this case, you likely want to match just on the host.
In the reverse proxy handler, you'll want to set your upstream to your app URL. In some cases, you might want to add some extra paths on there, but beware that can have some unexpected behavior because every request will be sent to that path as the base URL, instead of the naked domain as the base URL.
There are a ton of things you can set in the handler, each of which could be their own article, so I won't go over them here because they aren't absolutely required. But explore the docs to see what you might need!
The other thing you'll probably want to have is on-demand TLS turned on. You can see the docs for that here.
This will wait to attempt to grab an SSL/TLS cert until the first request hits the Caddy instance for a domain in the config, instead of trying on startup. That can be very handy when your user may not have pointed their DNS yet, since the certificate authorities have pretty strict rate limits you could run into trying to verify the domain.
If you decide to cluster, each instance inside the cluster will need to coordinate so that certificate authorities trying to verify a domain for SSL issuance aren't getting different responses from different instances. They use servers all over the world, so you can't guarantee it'll hit the same server that asked for the cert. That's outside the scope of this article, but it can be really hard to debug so I wanted to mention it quickly.
Finally, if you're using on-demand TLS, you'll need to create an Ask endpoint in your app that Caddy can hit to ensure that it should actually allow that domain. It's an extra security/rate limit protection measure to make sure you're not serving domains you don't intend to. You can find docs on that here.
Okay, so lets say you've got a Caddy instance and it's automatically updating the configuration for custom domains, and now requests for that custom domain are secured and reaching your app.
It's probably not working yet (oh no!).
That's likely because your app doesn't know a few things yet. Most frameworks will have at least a few of these:
Generally, you need to figure out how to allow (as in, not block) domains other than the primary domain in your framework. Usually that can be done somewhere like a router, or with middleware, and often requires some config change to stop hard-coding it.
Once you've done that, you'll need to setup some route groups or middleware (or whatever equivalents your framework has) to separate out requests for the primary app vs requests for custom domains.
With Approximated, we've created a few example repos that you can use as an example for how to handle this in those frameworks:
Alternatively, you could have a separate app that handles only custom domain requests, but most folks have already built features to handle specific sub directories or subdomains so it's easier to do it all in one app.
You'll also want to let your user's know how to point their domain at your Caddy instance. Typically you'll want an A record or a CNAME record. Either is fine but each has trade-offs.
Here's the list again of the things that you need and that we've covered in this guide:
Once that's all setup, you should be able to create custom domains, secure them with TLS/SSL certs on demand, and handle requests for them that return the appropriate responses for your app! Woohoo!
So that's everything for now! I'll update this as needed, but hopefully this can get you started on at least trying out custom domains as a feature. And again, if you'd rather not build this yourself, you can always use Approximated.