Redirect parts of your website to different applications with Apache
Microservices everywhere. Those are the times we live in now. Everyone seems to be splitting monolithic web applications into smaller chunks. And that's fine. However, setting up the local development environment for this can be sometimes a bit cumbersome.
It’s not an uncommon scenario when splitting parts of your site into standalone services to want to redirect certain paths of your URL to different back-end systems. There are multiple ways to do this, and I’ll show you how to set up a relatively painless Apache 2.4 setup where you can serve your monolith application as you used to, but at the same time point the new bits to the new services.
Let’s imagine the following scenario:
- You're developing a site
http://www.acme.com
- In your local development environment, you have a Rails app, serving the monolith, in port
3000
- In order to make it all as close to production as possible, you develop in a special development domain
dev.acme.com
Step 1: serving the monolith
The first thing you need to do is ensure your domain dev.acme.com
points to your localhost
:
At this point, with your Rails app up and running, if you point your browser to http://dev.acme.com:3000
, you should see your monolith app. However, we want to get rid of the port in that URL. We can achieve this by using the Apache mod_proxy
plugin.
Let’s start by creating our own Apache configuration file that we can mess with, so we don’t have to worry about modifying the ones that come with our OS.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<VirtualHost *.dev.acme.com:80>
ServerName www.dev.acme.com
ServerAlias *.dev.acme.com
ServerAlias dev.acme.com
ProxyRequests Off
# Monolith
ProxyPass / http://0.0.0.0:3000/ retry=0
ProxyPassReverse / http://0.0.0.0:3000/
<Location "/">
# Allow access to this proxied URL location for everyone.
Order allow,deny
Allow from all
</Location>
<VirtualHost *.dev.acme.com:80>
The first thing we do is create a VirtualHost
that will apply to any request coming to dev.acme.com
on port 80
(the default HTTP port) We do that on lines 2, 3 and 4, where we set up the ServerName
and a few ServerAlias
. The ServerName
directive will match requests coming to that domain into the VirtualHost
. Since we usually want some other subdomains to be served by the same application, we can add aliases to the name via the ServerAlias
directive. These lines will cover any request made to the dev.acme.com
domain or any of its subdomains.
On line 5 we disable the forward proxy feature. No need to worry too much about this though, as it’s out of the scope of this post and this is just a local setup.
Then we make the magic happen in lines 8 and 9, using ProxyPass
and ProxyPassReverse
. ProxyPass / http://0.0.0.0:3000/ retry=0
will tell Apache to get any requests coming to the server and send them to our Rails application, which lives in port 3000
of our local machine. The retry=0
bit will ensure that requests will not be waiting for a timeout if for whatever reason our backend server was not available (e.g. our Rails app is starting, or has crashed).
Unfortunately this directive is not enough. When the request reaches your local Rails application, it is served back to Apache, but because it’s still being served at localhost:3000
, some of its headers may contain references to this domain. One good example is a redirect response, which would be served back as a 302
or 301
with a Location
header of http://localhost:3000/new_url
.
Here’s where the ProxyPassReverse / http://0.0.0.0:3000/
comes into play. This line tells Apache to transform any response from the backend to modify the Location
, Content-Location
and URI
headers and replace them by the host used in the original request (e.g. dev.acme.com
). This way the redirect response from above would become a 302
or 301
with a Location
header of http://dev.acme.com/new_url
.
Step 2: splitting part of the site into its own service
Let’s say we want to change the way we serve part of our site. We’ve traditionally served our product category pages from the monolith, via controller requests making some MySQL queries and eventually rendering some ERB views. But now the information on these pages is actually coming from a API, and we want to render them via a new React Single Page Application. We therefore want all pages under the /categories
path on our website to be served by this new nodejs server when doing local development, so everything looks as transparently and close to production as possible.
We will assume that our front-end development server lives on localhost:4000
, so the setup we are after is the following:
- Requests made to
http://dev.acme.com/categories
or any sub path of it need to be served by the nodejs front-end application on port4000
- Requests made elsewhere in the domain
http://dev.acme.com/categories
need to be served by our Rails application on port3000
In order for us to achieve this we need to add another set of proxy rules to our Apache configuration file:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<VirtualHost *.dev.acme.com:80>
ServerName www.dev.acme.com
ServerAlias *.dev.acme.com
ServerAlias dev.acme.com
ProxyRequests Off
# Front-end app
ProxyPass "^/categories.*$" http://0.0.0.0:4000/ retry=0
ProxyPassReverse /categories http://0.0.0.0:4000/
# Monolith
ProxyPass / http://0.0.0.0:3000/ retry=0
ProxyPassReverse / http://0.0.0.0:3000/
<Location "/">
# Allow access to this proxied URL location for everyone.
Order allow,deny
Allow from all
</Location>
<VirtualHost *.dev.acme.com:80>
On lines 8 and 9, we tell Apache to look for any path matching the regular expression ^/categories.*$
and route it to the server listening on port 4000
. You’ll have noticed that we added the rules for the category pages before the rules for the root path. This is important, as Apache will look at rules in order. If we were to swap lines 8 and 9 for lines 12 and 13, when visiting /categories
, Apache would match that route to /
, and it’d be served by our Rails app.
If you restart Apache now, fire up both your Rails and nodejs applications, and visit http://dev.acme.com/categories
, you’ll see on your nodejs logs how the request has indeed been routed to the application on port 4000
. However, you’ll probably notice that the application is not actually loading properly on your browser. If you go and then have a look at your Rails logs, you may notice a request being made to the path /bundle.js
.
What’s happening here? Why is that React javascript file being routed to the Rails app? Let’s look at the whole process, step by step:
- A request is made to
http://dev.acme.com/categories
- Apache routes it to your nodejs server
- Your nodejs server replies with an html response
- Apache receives this response and replaces its headers, ensuring any reference to
localhost:4000
gets transformed intohttp://dev.acme.com
- Apache sends this modified response to the browser
- The browser renders the contents of the response
If you are using a typical React project, the HTML response that your browser renders will look like this:
1
2
3
4
5
6
7
8
9
10
11
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="/style/style.css">
</head>
<body>
<div class="container"></div>
</body>
<script src="/bundle.js"></script>
</html>
Lines 5 and 10 are the issue here. Your browser will try to load the bundled assets referenced in your React app from the root of your path. This means the browser will make and HTTP request to load these files using the following URLs:
http://dev.acme.com/style/style.css
http://dev.acme.com/bundle.js
When Apache gets the requests, it will try to match them against the regular expression ^/categories.*$
, there will be no match, and therefore it will route the request to your Rails app. And your Rails app knows nothing about these files, as they don’t belong to it.
We could now add more proxy rules to our configuration, but this doesn’t scale very well the moment we need to either add more applications, or our React app needs to serve more assets. Luckily for us, there’s another tool we can use to help us: Apache’s mod_rewrite
plugin. mod_rewrite provides us with directives that can change the paths of the requested URLs based on rules. We can do this with RewriteCond
and RewriteRule
. This is what our final Apache configuration file will look like:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<VirtualHost *.dev.acme.com:80>
ServerName www.dev.acme.com
ServerAlias *.dev.acme.com
ServerAlias dev.acme.com
ProxyRequests Off
RewriteEngine On
# Front-end app
ProxyPass "^/categories.*$" http://0.0.0.0:4000/ retry=0
ProxyPassReverse /categories http://0.0.0.0:4000/
RewriteCond "%{HTTP_REFERER}" http://dev.acme.com/categories.*
RewriteRule .(js|css)$ http://localhost:4000/%{REQUEST_URI} [R=301,L]
# Monolith
ProxyPass / http://0.0.0.0:3000/ retry=0
ProxyPassReverse / http://0.0.0.0:3000/
<Location "/">
# Allow access to this proxied URL location for everyone.
Order allow,deny
Allow from all
</Location>
<VirtualHost *.dev.acme.com:80>
On line 6 we enable the rewrite engine, so we can use the mod_rewrite directives. And then on lines 11 and 12 we make it all work. Whenever a request to load any file coming from the nodejs server is sent to Apache, it has an HTTP header called HTTP_REFERRER
. This header contains the URL that made the request. In our case, any requests coming from the nodejs app will be coming from the /categories
path, as it’s the only path our front-end application is serving. Let’s have a closer look at lines 11 and 12.
RewriteCond "%{HTTP_REFERER}" http://dev.acme.com/categories.*
will add a Rewrite Condition. Rewrite Conditions are evaluated by Apache, and if they match, then a Rewrite Rule gets applied. We only have one condition in our case, and the condition is to check if the HTTP_REFERER
header of the request matches the regular expression http://dev.acme.com/categories.*
.
RewriteRule (.js|css)$ http://localhost:4000/%{REQUEST_URI} [R=301,L]
is the actual rule that will get applied if the conditions before are met. The rule gets any file ending in js
or css
, and then issues a redirect response to a modified URL so that file is requested from the root of http://localhost:4000
. This way, a request to http://dev.acme.com/bundle.js
gets a 301
redirect response to http://localhost:4000/bundle.js
, which our nodejs server will happily serve back.
Summary
We can use Apache to help us develop complex multi service applications locally. In order to do that, we need to spin up our different services to listen to different ports, and tell Apache to route requests to the correct application via ProxyPass
and ProxyPassReverse
directives, ensuring each path of our URL gets served by the right service. For more complex applications, where individual services need to load assets over HTTP, we can use the RewriteCond
and RewriteRule
directives so these assets get served by the correct services as well. Finally, we can wrap it all up inside a VirtualDomain
and an extra DNS line in our /etc/hosts
file, to be able to develop in an environment that is very close to what our production setup will look like.