Deploy a multi-component web application with nginx
The goal of this exercise is to understand the challenges of deploying a
multi-component web application, and how a reverse proxy like nginx can help.
This guide assumes that you are familiar with reverse proxying, that you have nginx installed and
running on a server, and that you have a DNS wildcard entry preconfigured to
make various subdomains (*.jde.archidep.ch in this guide) point to that
server.
On this pageTable of contents
Legend
Parts of this exercise are annotated with the following icons:
A task you MUST perform to complete the exercise
An optional step that you may perform to make sure that everything is working correctly, or to set up
additional tools that are not required but can help you
The end of the exercise
The architecture of the software you ran or deployed during this exercise.
Troubleshooting tips: how to fix common problems you might encounter
The application
The application you will deploy is Revprod, a marketing web application
where customers can leave testimonials about The Revolutionary Product. This
application has been developed as two separate components:
The revprod landing
page: the main page of
the application that describes the product and displays customers’
testimonials.
This component is basically a static page with no server-side logic. It loads
testimonials from the backend using AJAX requests.
The revprod backend: an
application that allows customers to provide their testimonials.
This component stores the customers’ testimonials in an embedded file
database.
This separation is for the purposes of the exercise, but large applications are
often split like this for various reasons.
Some advantages of a multi-component application are:
Each component can be developed and deployed separately.
Each component could be developed by a separate team, using their favorite
programming language and tools.
Each team could deploy new versions of their component independently.
The disadvantages are:
It is more complex to manage the development and
deployment of a multi-component application.
Separate teams working together must agree on the API that the components use
to communicate and not break that contract.
On the deployment side, you have to make sure that you always deploy
compatible versions of all components together.
Install Node.js
As described in the READMEs of both application, you need Node.js version 24
installed on your server to run them. Use the following commands to install
the correct version of Node.js:
Let’s start by deploying the revprod backend and frontend separately at these
URLs (replacing jde with your name as you configured it during the DNS
exercise):
http://revprod-backend.jde.archidep.ch
http://revprod-landing.jde.archidep.ch
Deploy the revprod landing page
Clone the landing page repository on your server and install the required
dependencies:
$>cd$> git clone https://github.com/ArchiDep/revprod-landing-page.git
$>cd revprod-landing-page
$> npm ci
Create a systemd unit file named /etc/systemd/system/revprod-landing.service
(e.g. with nano) to execute this component and make it listen on port 4201:
[Unit]
Description=LandingpageforTheRevolutionaryProduct
[Service]
ExecStart=/usr/bin/nodebin.jsWorkingDirectory=/home/jde/revprod-landing-pageEnvironment="REVPROD_LISTEN_PORT=4201"# Public URL at which the backend can be accessed
Environment="REVPROD_BACKEND_BASE_URL=http://revprod-backend.jde.archidep.ch"User=jdeRestart=on-failure
[Install]
WantedBy=multi-user.target
Tip
Replace jde with your name in the WorkingDirectory and User options, as
well as jde in the second Environment option indicating the URL of the
landing page.
You should then be able to access the revprod landing page at
http://revprod-landing.jde.archidep.ch.
Deploy the revprod backend
Clone the backend repository on your server and install the required
dependencies:
$>cd$> git clone https://github.com/ArchiDep/revprod-backend.git
$>cd revprod-backend
$> npm ci
Create a systemd unit file named /etc/systemd/system/revprod-backend.service
(e.g. with nano) to execute this component and make it listen on port 4200:
[Unit]
Description=BackendforTheRevolutionaryProduct
[Service]
ExecStart=/usr/bin/nodebin.jsWorkingDirectory=/home/jde/revprod-backendEnvironment="REVPROD_LISTEN_PORT=4200"# Public URL at which the frontend can be accessed
Environment="REVPROD_LANDING_PAGE_BASE_URL=http://revprod-landing.jde.archidep.ch"User=jdeRestart=on-failure
[Install]
WantedBy=multi-user.target
Tip
Replace jde with your name in the WorkingDirectory and User options, as
well as jde in the second Environment option indicating the URL of the
landing page.
You should then be able to access the revprod backend at
http://revprod-backend.jde.archidep.ch.
Take the time to share your thoughts about The Revolutionary Product!
It’s not working!
If you have followed the instructions so far, you should be able to access the
revprod backend and landing page in your browser. You should also be able to
create testimonials in the backend page.
Note that the URL switches from http://revprod-landing.jde.archidep.ch to
http://revprod-backend.jde.archidep.ch (and back) when you navigate from
the landing page to the Share page. Both components use exactly the same theme
so that the transition is seamless, however by looking at the URL the user
can clearly see that these are two separate sites.
But more importantly, the testimonials are not displayed on the landing
page!
If you open your browser’s developer console, you should see an error that looks
something like this:
Cross-Origin Request Blocked: The Same Origin Policy disallows reading the
remote resource at http://revprod-backend.jde.archidep.ch/comments.
(Reason: CORS header ‘Access-Control-Allow-Origin’ missing). Status code: 200.
The landing page’s AJAX request to fetch the comments from the backend has been
blocked by the browser because the request is to a different origin: the
landing page is at http://revprod-landing.jde.archidep.ch and is attempting to
access http://revprod-backend.jde.archidep.ch which is another domain
entirely.
This is called the Same-Origin Policy. It is a critical security
mechanism that restricts how a document or script loaded by one origin can
interact with a resource from another origin.
It helps isolate potentially malicious documents, reducing possible attack
vectors. For example, it prevents a malicious website on the Internet from
running JS in a browser to read data from a third-party webmail or e-banking
service (which the user is signed into) or a company intranet (which is
protected from direct access by the attacker by not having a public IP address)
and relaying that data to the attacker.
Using Cross-Origin Request Sharing (CORS)
One way to solve this issue is with Cross-Origin Request Sharing (CORS):
the backend can use HTTP response headers to indicate to the frontend that it
can perform requests from a different origin.
The revprod backend already supports sending the appropriate CORS headers to
allow cross-origin requests. Update the systemd unit file
/etc/systemd/system/revprod-backend.service for the backend and add the
appropriate environment variables to the [Service]
section to enable CORS:
Refresh the revprod landing page at
http://revprod-landing.jde.archidep.ch again. The comments should work
this time!
If you look at your browser’s developer console when refreshing the page, you
should see that the backend now sends the following header in the comments
response:
Check that the comments no longer work by refreshing
http://revprod-landing.jde.archidep.ch.
Tip
You may need to force a refresh by holding the Shift key.
Using nginx to make both components appear as a single website
The problem we have is that our two components are deployed on separate domains,
therefore a request from the landing page to the backend is a cross-origin
request and is blocked by the same-origin policy by default.
What if we had only one domain, and therefore only one origin?
A reverse proxy like nginx is a very powerful tool. You have so far configured
two separate nginx sites with separate proxies to the backend and landing page,
but nothing says it has to be that way. You can actually configure one site to
proxy to both components depending on various criteria.
Let’s assume that we want the revprod application (both the backend and the
landing page) to be accessible at one URL:
http://revprod.jde.archidep.ch.
Since the backend and landing page will be accessible at the same URL, we have
to update their configurations to reflect that fact. Update the systemd unit
file /etc/systemd/system/revprod-backend.service for the backend and comment
out (or remove) the REVPROD_LANDING_PAGE_BASE_URL environment variable in the
[Service] section:
You must create a new nginx site configuration file
/etc/nginx/sites-available/revprod. This site configuration must fulfill
the following criteria:
There must be only one server block.
The listen directive must still use port 80 like the previous
configurations in this exercise.
The server_name directive must be revprod.jde.archidep.ch (replacing
jde with your name).
The root directive must be the same as the one from the landing page’s site
configuration.
There must be multiple location blocks in the server block, to serve
both the backend and frontend components, each with their own proxy_pass
directive. These blocks will be similar to but not exactly the same as those
used earlier in the exercise. You must make sure the following holds true:
Requests to / are proxied to the landing page.
Requests to /comments or /share are proxied to the backend.
Tip
A location block can match specific requests depending on how you write it.
Read the “Configuring Locations” section of Configuring nginx as a Web
Server.
You should now be able to access the revprod application at
http://revprod.jde.archidep.ch and everything should work!
If your new site configuration is correct, note that your are no longer
switching from http://revprod-landing.jde.archidep.ch to
http://revprod-backend.jde.archidep.ch when navigating in the
application. Everything is served under http://revprod.jde.archidep.ch
because everything goes through nginx which then proxies it internally to
our separate components.
Neither the browser nor the user now have any idea that this application is in
fact composed of multiple components. To the outside world, it appears as one
application on one domain, thus also solving our original problem: there is no
longer any cross-origin request, so the same-origin policy does not apply.
What have I done?
You have deployed a multi-component website in a way that makes it appear
as a single website to the end user. You have achieved this by running each
component separately, and then configuring your reverse proxy (nginx) to
appropriately proxy requests to each component.
Architecture
This is a simplified architecture of the main running processes and
communication flow at the end of this exercise:
Note
Note that this diagram only shows the processes involved in this exercise,
ignoring the other applications (such as the PHP Todolist) we have also deployed
on the server.
Troubleshooting
Here’s a few tips about some problems you may encounter during this exercise.
nginx: [emerg] could not build server_names_hash
If you encounter the following error:
$> sudo nginx -t
nginx: [emerg] could not build server_names_hash, you should increase server_names_hash_bucket_size: 64
nginx: configuration file /etc/nginx/nginx.conf test failed
It may be because your domain name (the value of your server_name directive)
is too long for nginx’s default settings. In that case, edit the main nginx
configuration with sudo nano /etc/nginx/nginx.conf and add the following line
in the http section: