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
(*.john-doe.archidep.ch
in this guide) point to that server.
- Legend
- 💎 Requirements
- 💎 The application
- ❗ Deploy the components separately
- ❗ It's not working!
- ❗ Using Cross-Origin Request Sharing (CORS)
- ❗ Using nginx to make both components appear as a single website
- 🏁 What have I done?
- 💥 Troubleshooting
Parts of this guide 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.
⚠️ Critically important information about the exercise.- 💎 Tips on the exercise, reminders about previous exercises, or explanations about how this exercise differs from the previous one.
- 👾 More advanced tips on how to save some time. Challenges.
- 📚 Additional information about the exercise or the commands and tools used.
- 🏁 The end of the exercise.
- 🏛️ The architecture of what you deployed during the exercise.
- 💥 Troubleshooting tips: how to fix common problems you might encounter.
The following requirements must be installed on your server:
- Node.js 20.x (installation instructions)
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.
Let's start by deploying the revprod backend and frontend separately at these
URLs (replacing john-doe
with your name as you configured it during the DNS
exercise):
http://revprod-backend.john-doe.archidep.ch
http://revprod-landing.john-doe.archidep.ch
Clone the landing page repository on your server and install the required dependencies:
$> cd
$> git clone https://github.com/MediaComem/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=Landing page for The Revolutionary Product
[Service]
ExecStart=/usr/bin/node bin.js
WorkingDirectory=/home/john_doe/revprod-landing-page
Environment="REVPROD_LISTEN_PORT=4201"
# Public URL at which the backend can be accessed
Environment="REVPROD_BACKEND_BASE_URL=http://revprod-backend.john-doe.archidep.ch"
User=john_doe
Restart=on-failure
[Install]
WantedBy=multi-user.target
💎 Replace
john_doe
with your name in theWorkingDirectory
andUser
options, as well asjohn-doe
in the secondEnvironment
option indicating the URL of the landing page.
Enable and start your new service:
$> sudo systemctl enable revprod-landing
$> sudo systemctl start revprod-landing
💎 You can check that it is running with
sudo systemctl status revprod-landing
.
Create an nginx site configuration file
/etc/nginx/sites-available/revprod-landing
(e.g. with nano
) to expose this
component:
server {
listen 80;
server_name revprod-landing.john-doe.archidep.ch;
root /home/john_doe/revprod-landing-page/public;
location / {
proxy_pass http://127.0.0.1:4201;
}
}
💎 Replace
john-doe
with your name in theserver_name
directive, as well asjohn_doe
in theroot
directive.
Enable that configuration with the following command:
$> sudo ln -s /etc/nginx/sites-available/revprod-landing /etc/nginx/sites-enabled/revprod-landing
Check and reload nginx's configuration:
$> sudo nginx -t
$> sudo nginx -s reload
You should then be able to access the revprod landing page at http://revprod-landing.john-doe.archidep.ch.
Clone the backend repository on your server and install the required dependencies:
$> cd
$> git clone https://github.com/MediaComem/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=Backend for The Revolutionary Product
[Service]
ExecStart=/usr/bin/node bin.js
WorkingDirectory=/home/john_doe/revprod-backend
Environment="REVPROD_LISTEN_PORT=4200"
# Public URL at which the frontend can be accessed
Environment="REVPROD_LANDING_PAGE_BASE_URL=http://revprod-landing.john-doe.archidep.ch"
User=john_doe
Restart=on-failure
[Install]
WantedBy=multi-user.target
💎 Replace
john_doe
with your name in theWorkingDirectory
andUser
options, as well asjohn-doe
in the secondEnvironment
option indicating the URL of the landing page.
Enable and start your new service:
$> sudo systemctl enable revprod-backend
$> sudo systemctl start revprod-backend
💎 You can check that it is running with
sudo systemctl status revprod-backend
.
Create an nginx site configuration file
/etc/nginx/sites-available/revprod-backend
(e.g. with nano
) to expose this
component:
server {
listen 80;
server_name revprod-backend.john-doe.archidep.ch;
root /home/john_doe/revprod-backend/public;
location / {
proxy_pass http://127.0.0.1:4200;
}
}
💎 Replace
john-doe
with your name in theserver_name
directive, as well asjohn_doe
in theroot
directive.
Enable that configuration with the following command:
$> sudo ln -s /etc/nginx/sites-available/revprod-backend /etc/nginx/sites-enabled/revprod-backend
Check and reload nginx's configuration:
$> sudo nginx -t
$> sudo nginx -s reload
You should then be able to access the revprod backend at http://revprod-backend.john-doe.archidep.ch.
Take the time to share your thoughts about The Revolutionary Product!
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 be able to create testimonials in the backend page.
Note that the URL switches from http://revprod-landing.john-doe.archidep.ch
to
http://revprod-backend.john-doe.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.john-doe.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.john-doe.archidep.ch
and is
attempting to access http://revprod-backend.john-doe.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.
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:
Environment="REVPROD_CORS=true"
Environment="REVPROD_CORS_ORIGINS=http://revprod-landing.john-doe.archidep.ch"
💎 Replace
john-doe
with your name in the definition of the second environment variable.
Reload the Systemd configuration and restart the backend service:
$> sudo systemctl daemon-reload
$> sudo systemctl restart revprod-backend
Refresh the revprod landing page at http://revprod-landing.john-doe.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:
Access-Control-Allow-Origin: http://revprod-landing.john-doe.archidep.ch
Your browser knows to check this header and let the request through if the origin matches.
You should now disable CORS because we will explore another solution to this problem during the rest of this exercise.
Update the Systemd unit file /etc/systemd/system/revprod-backend.service
for
the backend and set the value of the REVPROD_CORS
variable to false:
Environment="REVPROD_CORS=false"
Reload the Systemd configuration and restart the backend service:
$> sudo systemctl daemon-reload
$> sudo systemctl restart revprod-backend
Check that the comments no longer work by refreshing http://revprod-landing.john-doe.archidep.ch.
💎 You may need to force a refresh by holding the Shift key.
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.john-doe.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:
#Environment="REVPROD_LANDING_PAGE_BASE_URL=http://revprod-landing.john-doe.archidep.ch"
Do the same in the Systemd unit file
/etc/systemd/system/revprod-landing.service
for the landing page for the
REVPROD_BACKEND_BASE_URL
variable:
#Environment="REVPROD_BACKEND_BASE_URL=http://revprod-backend.john-doe.archidep.ch"
Reload Systemd's configuration and restart both services to take these changes into account:
$> sudo systemctl daemon-reload
$> sudo systemctl restart revprod-backend
$> sudo systemctl restart revprod-landing
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 port80
like the previous configurations in this exercise. -
The
server_name
directive must berevprod.john-doe.archidep.ch
(replacingjohn_doe
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 theserver
block, to serve both the backend and frontend components, each with their ownproxy_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.
💎 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 can read Understanding Nginx Server and Location Block Selection Algorithms if you want more detailed information.
- Requests to
Enable the new configuration with the following command:
$> sudo ln -s /etc/nginx/sites-available/revprod /etc/nginx/sites-enabled/revprod
Check and reload nginx's configuration:
$> sudo nginx -t
$> sudo nginx -s reload
You should now be able to access the revprod application at http://revprod.john-doe.archidep.ch and everything should work!
If your new site configuration is correct, note that your are no longer
switching from http://revprod-landing.john-doe.archidep.ch
to
http://revprod-backend.john-doe.archidep.ch
when navigating in the
application. Everything is served under http://revprod.john-doe.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.
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.
This is a simplified architecture of the main running processes and communication flow at the end of this exercise (after completing all previous course exercises):
Here's a few tips about some problems you may encounter during this exercise.
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:
server_names_hash_bucket_size 256;