Estimated reading time: 3 mins

Hosting multiple Heroku apps on a single domain

In this guest engineering post from Plane's CEO Matt Pelc, he discusses how a single application on Heroku can have any number of domains assigned to it, but you can only add a domain to one app.

Close-up of a piece of code

Matt Pelc

A single application on Heroku can have any number of domains assigned to it, but you can only add a domain to one app. This means that by default you can’t serve example.com from the example-1 while example.com/blog is served from example-2.We ran into this problem with plane.com recently where we have a constellation of apps (pilot-co, pilot-blog, pilot-stories, etc.) which we wanted to host under a single domain.We found a way to do that by putting a custom HAProxy instance, also hosted on Heroku, in front of all other Heroku apps we use.

1. Set up

Let’s say you have two apps on Heroku already:
  • example-com running https://example.com
  • example-blog running https://blog.example.com
We will need a new app for your load balancer:
1$ mkdir load-balancer
2$ cd load-balancer
3$ git init .
Then create an app on Heroku:
1$  heroku apps:create example-lb
2Creating example-lb... done, stack is cedar-14
3http://example-lb.herokuapp.com/ | https://git.heroku.com/example-lb.git
4Git remote heroku added

2. Installing Docker

You will deploy it to Heroku using Docker. We found it to be easier to manage than creating a custom buildpack.Luckily, installing Docker on your machine is easy. Get Docker Toolbox and follow its setup instructions.To verify that you have a working Docker installation, open your terminal and run:
1docker ps
2CONTAINER ID        IMAGE               COMMAND ...
3$ docker-compose --version
4docker-compose version: 1.4.0
To deploy a Docker container to Heroku you will need heroku-docker:
1$ heroku plugins:install heroku-docker
Heroku requires an app.json and Procfile manifests to be able to run your app.
1{
2  "name": "Pilot Load Balancer",
3  "description": "A load balancer for pilot.co",
4}
Your Procfile should look something like this:
1web: sbin/haproxy -f haproxy.cfg
Then initialise Docker assets for the app:
1$ heroku docker:init
2Wrote Dockerfile
3Wrote docker-compose.yml

3. Configuring HAProxy

Your Dockerfile is where we add instructions for Heroku on how to compile HAProxy:
1FROM heroku/cedar:14
2RUN mkdir -p /app/user
3WORKDIR /app/user
4# Install HAProxy
5RUN apt-get update && apt-get install -y libssl1.0.0 libpcre3 --no-install-recommends && rm -rf /var/lib/apt/lists/*
6ENV HAPROXY_MAJOR 1.5
7ENV HAPROXY_VERSION 1.5.14
8ENV HAPROXY_MD5 ad9d7262b96ba85a0f8c6acc6cb9edde
9# see http://sources.debian.net/src/haproxy/1.5.8-1/debian/rules/ for some helpful navigation of the possible "make" arguments
10RUN buildDeps='curl gcc libc6-dev libpcre3-dev libssl-dev make' \
11	&& set -x \
12	&& apt-get update && apt-get install -y $buildDeps --no-install-recommends && rm -rf /var/lib/apt/lists/* \
13	&& curl -SL "http://www.haproxy.org/download/${HAPROXY_MAJOR}/src/haproxy-${HAPROXY_VERSION}.tar.gz" -o haproxy.tar.gz \
14	&& echo "${HAPROXY_MD5}  haproxy.tar.gz" | md5sum -c \
15	&& mkdir -p /app/user/src/haproxy \
16	&& tar -xzf haproxy.tar.gz -C /app/user/src/haproxy --strip-components=1 \
17	&& rm haproxy.tar.gz \
18	&& make -C /app/user/src/haproxy \
19		TARGET=linux2628 \
20		USE_PCRE=1 PCREDIR= \
21		USE_OPENSSL=1 \
22		USE_ZLIB=1 \
23    PREFIX=/app/user \
24		all \
25		install-bin \
26	&& rm -rf /app/user/src/haproxy \
27	&& apt-get purge -y --auto-remove $buildDeps
28COPY haproxy.cfg /app/user/haproxy.cfg
One last thing we need to do is configure HAProxy to route requests from our main app (called frontend) to all other apps (called backends).HAProxy’s configuration manual is relatively easy to understand, and after some fine-tuning you should end up with something like this:
1global
2    maxconn 256
3defaults
4    mode http
5    timeout connect 5000ms
6    timeout client 50000ms
7    timeout server 50000ms
8frontend http
9    bind 0.0.0.0:$PORT
10    option forwardfor
11    # Force SSL
12    redirect scheme https code 301 if ! { hdr(x-forwarded-proto) https }
13    # Redirect all requests to /blog* to the example-blog app.
14    use_backend example-blog if { path_beg /blog }
15    # And all other requests to example-com.
16    default_backend pilot-com
17backend pilot-com
18    http-request set-header X-Forwarded-Host example.com
19    http-request set-header X-Forwarded-Port %[dst_port]
20    reqirep ^Host: Host:\ example-com.herokuapp.com    
21    server example-com example-com.herokuapp.com:443 ssl verify none
22backend example-blog
23    http-request set-header X-Forwarded-Host example.com
24    http-request set-header X-Forwarded-Port %[dst_port]
25    reqirep ^Host: Host:\ example-blog.herokuapp.com
26    server example-blog example-blog.herokuapp.com:443 ssl verify none
You can verify your setup locally by starting Docker:
1$ docker-compose up web
and opening the browser:
1$ open "http://$(docker-machine ip default):8080"

4. Deploying your load balancer to Heroku

If you’re satisfied with the outcome, it’s time to deploy it to Heroku:
1heroku docker:release
2heroku open
After you verified that your new setup works on https://example-lb.herokuapp.com, you can remove the example.com domain from example-com and attach it to example-lb.

5. After you’re done

  • Requests to https://example.com will go through example-lb and be served from example-com.
  • Requests to https://example.com/blog will also go through example-lb but be served from example-blog instead.
  • All this will be completely hidden from your users. At no point they should see example-blog.herokuapp.com or any domain other than example.com.
If you’re using SSL (which this guide assumes you were) you can safely remove the SSL add on from all apps other than example-lb. Traffic between Heroku apps will be encrypted using their *.herokuapp.com certificate.

Additional resources

Build and Deploy with Docker on HerokuHAProxy Configuration Manual

Related articles

From startups to large corporations, US companies of all sizes use Plane for global payroll, benefits and compliance.

Want industry news and product updates?Sign up for our newsletter.