Motivation
I have a bunch of small (toy?) apps hosted on Heroku. They are probably used by a few dozen people, at most. For these apps, I’d like the following setup:
-
Use a custom domain for the apps
-
Heroku provides domains of the form
appname.herokuapp.com
for all apps. -
But, it also has the option to add one or more custom domains
-
I usually already have a domain under which I want to add a sub-domain, where these apps would run.
-
-
Use HTTPS for all connections
-
The domains or the sub-domains usually don’t already already have an SSL certificate. With lets-encrypt, I guess this can change in future.
-
Heroku provides SSL certificates for the default domain (
appname.herokuapp.com
) for free. -
They also have an option to buy SSL certificates for custom domains, but they are expensive!
-
I’m going to use Cloudflare’s free SSL service instead. I’d like to have SSL for the full domain, and not just the app’s subdomain.
-
-
Redirect all the requests to the Heroku domain to the custom domain
-
Nobody should really be using the
appname.herokuapp.com
domain, in case I’d like to move away from it. -
A few lines of app (Flask) code can do this for us.
-
Setup
Setup DNS to use Cloudflare
Since we plan to use Cloudflare for the SSL certificates, we need to change DNS settings at our domain registrar to use Cloudflare. We first need to create a Cloudflare account, and let Cloudflare do a DNS scan to add all the existing domain settings automatically. Next we point to the Cloudflare DNS servers on our domain registrar.
Enable HTTPS always in Cloudflare
Using page rules in Cloudflare, add a rule for the whole domain to “Always use
HTTPS”. For example, use the url http://*example.com/*
and select the Always use HTTPS
option.
Select the SSL option to use
Cloudflare provides different options for the SSL setting which changes whether or not traffic is encrypted between Cloudflare and the Heroku app.
Choose Full (strict)
to enable encryption between Cloudflare and Heroku.
If you are not so concerned about encryption between Cloudflare and Heroku, you
could select the Flexible
option too.
Add a custom domain in Heroku
Add a custom domain – app.example.com
– in Heroku’s settings.
Heroku provides a DNS Target that needs to be used as the destination for a
CNAME setting in the DNS provider (Cloudflare). The DNS Target looks something
like foo-bar-123abcdef.herokudns.com
If you chose Full (strict)
SSL option, this DNS target cannot be used and can
be ignored.
Add a CNAME for the subdomain
Add a new CNAME setting in Cloudflare for app.example.com
and use the
app’s Heroku domain name (appname.herokuapp.com
) as the destination value/IP
address.
It is important to use the appname.herokuapp.com
value if the SSL settings you
chose above was Full (strict)
. Using the DNS Target provided by Heroku instead
(foo-bar-123abcdef.herokudns.com
) would give an SSL Handshake error since the
SSL certificate provided by Heroku only works for the appname.herokuapp.com
domain, and not for foo-bar-123abcdef.herokudns.com
.
If you chose Flexible
, though, you can use the DNS Target provided by Heroku
– foo-bar-123abcdef.herokudns.com
.
Also, ensure that the Cloudflare Proxy Toggle is toggled on – the cloud icon is orange, not grey!
Redirect all requests
We redirect all the requests coming to the old domain to the new one.
@app.before_request
def redirect_heroku():
"""Redirect herokuapp requests."""
urlparts = urlparse(request.url)
if urlparts.netloc == "appname.herokuapp.com":
urlparts_list = list(urlparts)
urlparts_list[1] = "app.example.com"
return redirect(urlunparse(urlparts_list), code=301)
Enforce HTTPS on Heroku
If you don’t care about a custom domain and just wish to enforce SSL for the
Heroku domain (appname.herokuapp.com
), Flask-SSLify
is the way to go. The
app can be hosted on Heroku and we can use the certs provided for free.
from flask_sslify import SSLify
app = Flask(__name__)
if "DYNO" in os.environ:
# Always use SSL if the app is running on Heroku (not locally)
sslify = SSLify(app)
Conclusion
I’ve done this setup, or some parts of it quite a few times, but each time I seem to need to look up the documentation. Every time, it seems to take longer than it needs to! Hopefully, this post will make it quick and reproducible.