Michal Zalecki
Michal Zalecki
software development, testing, JavaScript,
TypeScript, Node.js, React, and other stuff

Custom root domain and SSL on Heroku

Heroku managed to significantly lower the bar when it comes to deploying applications and integrating them with third-parties through various add-ons. It can be argued, but in my opinion optimizing platform to be easy at the entry-level made it much harder to do some more advanced setups. It’s strange but for quite a long time of using Heroku and feeling comfortable with it I’ve never had to configure it from A to Z. I was treating Heroku as some kind of rapid prototyping environment and finally migrating it to more dedicated solutions. That said, for one of the projects Heroku turned out to be a great fit and it made sense to keep it that way. So, now I only have to set up a root domain.

Add domain on Heroku

Firstly, we have to make Heroku aware which domain is going to be assigned to our application. I’m going to focus on common scenario, adding a root domain and www subdomain for the sake of showing the differences.

heroku domains:add example.com
heroku domains:add www.example.com

Remember to specify correct remote in case of using more than one. I’m used to use master branch as production and staging branch as... staging.

heroku domains:add example.com -r production
heroku domains:add staging.example.com -r staging

You can see your current config by calling heroku domains without any additional parameters.

$ heroku domains

Domain Name           DNS Target
───────────────  ──────────────────────────────────
example.com      example.com.herokudns.com
www.example.com  www.example.com.herokudns.com

Configure DNS

Next thing to take care of is DNS configuration. It’s super straightforward for subdomain as it only requires setting CNAME record.

CNAME	www.example.com	www.example.com.herokudns.com

Things get more tricky when it comes to the root domain. If you goggled this post you probably know what’s the problem. Heroku doesn’t provide static IP address for your application or to be more precise, Heroku claims IP can be changed to provide maximum uptime so you shouldn’t rely on IP address.

At this point is should be clear that using A record isn't reliable in the long run, although you can host example.com to check you app’s server IP. So, what’s the alternative to A record? You should use ALIAS/ANAME records depends on your DNS provider. The problem is that most of the domain registers like GoDaddy doesn’t allow you to set such record from your domain management panel. I’m not sure why is that way (I can guess, $$$). At this point I’d like to come up with something better than serving my app from www subdomain and redirect to www.* from root domain. That's the best you can do if you don't want to swich DNS server.

I’m going to use DNSimple as I like their clean UI and affordable pricing, but you will be good with any popular DNS provider. To switch to DNSimple you have to configure your domain’s DNS Servers to:

ns1.dnsimple.com
ns2.dnsimple.com
ns3.dnsimple.com
ns4.dnsimple.com

Those may change in the future (e.g. for new domains), so make sure those are ment for you.

Now, when you are waiting for a domain to switch to different DNS server you can add DNS entries. Example configuration can look like this:

Type   Name             Content
CNAME  www.example.com  www.example.com.herokudns.com
ALIAS  example.com      example.com.herokudns.com

It may take up to 24h for changes to take place but it’s more like 6h from my experience. You can check the progress on cachecheck.opendns.com. Keep in mind that setting your Heroku domain instead of Heroku’s DNS would work but it fail when you setup SSL.

Don't forget about cloning MX records so you keep your email working if you have any.

Obtain certificate

Update: Heroku introduced Automated Certificate Management. Service is available on Hobby and Professional dynos. ACM uses Let’s Encrypt to automatically generate and renew certificates for custom domains. You can skip to Enforce HTTPS if you decide to use it.

When it comes to certificates, we have two options: buy one or generate it with Let’s Encrypt. What is better for you depends on your needs but if you just want to have encrypted connection, use HTTP/2 or Service Workers then you should be fine with free certificate from Let’s Encrypt.

The easiest way to generate Let’s Encrypt certificate is using certbot. Unfortunately you won’t be able to perform auto-authorization as you can’t(?) run certbot on Heroku. Unlike on your own server with SSH access.

Install letsencrypt:

sudo apt-get install letsencrypt

or in case you are using macOS:

brew install certbot

Generate certificate, use manual flag. Running letsencrypt may require sudo.

letsencrypt certonly --manual --email [email protected] -d example.com -d www.example.com

Follow further instructions and verify your ownership of the domain to complete the process.

Install certificate

To upload SSL certificate we are going to use Heroku SSL addon which is enabled by default on all paid dynos. There is also paid alternative, SSL Endpoint, which is only relevant if you care about Firefox 2 or Internet Explorer 7, due to lack of SNI support, ouch!

heroku certs:add /etc/letsencrypt/live/example.com/fullchain.pem /etc/letsencrypt/live/example.com/privkey.pem --app example-app

Make sure you've changed the path to match your domain. The path where certifiicate is saved should be included in the success message you saw after generating it.

After certificate is added you see instructions how to configure DNS, but if you followed previous steps you should be already fine.

Enforce HTTPS

On Heroku SSL termination happens at load balancer, so your app has to check x-forwarded-proto header to determine either you are serving through HTTP or HTTPS.

Here’s an example of middleware which redirects requests to https.

function enforceHttps(req, res, next) {
  if (!req.secure &&
    req.get("x-forwarded-proto") !== "https" &&
    process.env.NODE_ENV === "production") {
    res.redirect(301, `https://${req.get("host")}${req.url}`);
  } else {
    next();
  }
}

app.use(enforceHttps);

Photo by rawpixel on Unsplash.