Reverse proxy explained (with nginx)

Reverse proxy explained (with nginx)

If you work as a developer or in any other IT related field, you've probably come across the term "reverse proxy". From my experience people are more often than not confused by the term. So what is a reverse proxy?

Maybe it's better to start of by explaining what is a forward proxy (often called proxy server or web proxy).

If there were no forward proxy, client would reach out directly to Server A (or B or C), and Server A would respond. However since forward proxy is in place, client will reach out to forward proxy (on a diagram above), forward proxy will reach out to Server A. When Server A responds, it will contact forward proxy and forward proxy will forward response to the client.

The question is why use this middleman when clients want to connect to the Internet. There are several reasons:

  • avoid browsing restriction - ex. some content is restricted based on country from which you are accessing it, forward proxy can get around this and let user connect to a proxy (ex. from some allowed country) rather than connecting directly to the visiting site.
  • block access to content - since it’s sitting in front of clients, forward proxy can also be used to restrict access to certain sites. (for ex. in schools or libraries)
  • to protect their online identity

OK, but what about reverse proxy?

In contrast to forward proxy which sits in front of the clients that are trying to connect, reverse proxy (also a server) sits in front of one or more origin servers, intercepting requests from the clients. The reverse proxy server will then send requests to and receive responses from the origin server.

If there were no reverse proxy in place, requests from the client would go directly to Server A (or B or C). With reverse proxy in place, all requests will go to reverse proxy, and reverse proxy will forward the requests to appropriate origin server - A. Reverse proxy will receive responses from A and forward to the client.

Again question arises of benefit of this schema? There are several benefits of using a reverse proxy:

  • load balancing - with reverse proxy in place you can distribute the traffic across a group of servers to maximize speed and utilize capacity. Also if one of the servers in the group goes offline, the load balancer redirects traffic to remaining online servers.
  • web acceleration - reverse proxy can also cache content as well as perform SSL encryption, taking the load off your web server, thus improving their performance.
  • security - since no request go directly to your backends, reverse proxy also protects their identities and makes harder for attacker to leverage a targeted attack against them (such as DDOS attack).

nginx - famous reverse proxy server

Nginx was created in 2004 by Russian developer Igor Sysoev.
Frustrated with Apache, and wanting to build a repleacement capable of handling 10.000 concurrent connections, with focus on performance, high concurrency and low memory usage.

Today, nginx serves the majority of world's top 1000 websites. This popularity, besides it's excellent performance, can be credited to it's relative ease of configuration and due to the fact it's relatively easy to start with.

Although often mentioned in the context of web servers, nginx, as it's core is a reverse proxy server, and it is because of this design that it performs so well.

We will use nginx to illustrate the reverse proxy mechanism.

Hands on

To "follow along" you can use any linux host, be it a cloud hosted VPS or a local VM with some version of linux, or WSL.
I am using an Ubuntu 20.04 on a WSL.

You can install nginx with the package manager :

apt-get install nginx
bojana@7OfNine:~$ nginx -v
nginx version: nginx/1.18.0 (Ubuntu)

To keep things simple, lets create a simple nginx configuration file in the home directory:

bojana@7OfNine:~$ touch nginx/nginx.conf

And let's add these lines to the config file:

events { }

http {

  server {
    listen 8888;

    location / {
     return 200 " Hello from NGINX\\n";
    }
  }
}

To understand the configuration file, let's discuss a few of a nginx configuration terms. Context - sections withing the configuration (namely events, http, server in our file) where directives can be set for that given context. You can think of a context as a scope. Directives - specific configuration option (ex. listen 8888), that gets set in the configuration file, and consists of a name and a value.

To start nginx with given configuration issue a command:

bojana@7OfNine:~$ nginx -c /home/bojana/nginx/nginx.conf

The -c argument is indicating that we will pass a configuration file, it's also worth noting that the path to the config file should be absolute. A good practice before reloading the nginx configuration is to verify if there is no syntax errors in the file itself (such as misplaced bracket or some misspelled directive or similar), kind of a dry run of an nginx configuration, to verify everything is correct (at least as far as syntax goes). To perform the test issue:

bojana@7OfNine:~/nginx/node$ nginx -t -c /home/bojana/nginx/nginx.conf
nginx: the configuration file /home/bojana/nginx/nginx.conf syntax is ok
nginx: configuration file /home/bojana/nginx/nginx.conf test is successful

We can see that everything is fine with the configuration file.
Let's test if the nginx is actually running by curl-ing the location:

bojana@7OfNine:~$ curl http://localhost:8888
 Hello from NGINX

To demonstrate nginx working as a reverse proxy, let's now create simple php server. Prerequisite to create a php server is that you have phpXx-cli package installed with Xx being the appropriate version. On my machine I installed php7.4-cli:

bojana@7OfNine:~$ sudo apt install php7.4-cli

After it's been installed, create a simple php server specifying host and a port like this:

bojana@7OfNine:~$ php -S localhost:9999
[Mon Jun 27 14:50:07 2022] PHP 7.4.3 Development Server (<http://localhost:9999>) started

PHP's builtin server defaults to serving the directory it's run from, so in our case that nginx directory.

We get a 404, because we didn't specify a path, nor we have index file in that root directory.

If we create the index.php file in that directory (for ex. with phpinfo function) and restart the php server from above. We will get the response.

<?php phpinfo(); ?>
bojana@7OfNine:~/nginx$ php -S localhost:9999
[Mon Jun 27 14:58:07 2022] PHP 7.4.3 Development Server (<http://localhost:9999>) started
[Mon Jun 27 14:58:10 2022] 127.0.0.1:48410 Accepted
[Mon Jun 27 14:58:10 2022] 127.0.0.1:48410 [200]: GET /

For the sake of simplicity let's just serve a simple text file:

bojana@7OfNine:~$ touch nginx/php_hello.txt
bojana@7OfNine:~$ echo "Hello from PHP" > nginx/php_hello.txt
bojana@7OfNine:~$ php -S localhost:9999 nginx/php_hello.txt
[Mon Jun 27 15:06:39 2022] PHP 7.4.3 Development Server (http://localhost:9999) started
....

bojana@7OfNine:~$ curl http://localhost:9999
Hello from PHP

Let's now say we want to access this php server via our nginx server. (the one on port 8888), for example by accessing  http://localhost:8888/php.

events { }

http {

  server {
    listen 8888;

    location / {
     return 200 " Hello from NGINX\\n";
    }

    location /php {
     proxy_pass "http://localhost:9999/";
    }

  }
}

We added new location (/php), and with directive "proxy_pass" we forwarded request to the php server.

Now let's reload the nginx configuration:

bojana@7OfNine:~$ sudo nginx -s reload

And let's test our reverse proxy by requesting that /php location :

bojana@7OfNine:~$ curl http://localhost:8888/php
Hello from PHP

We get a response from a PHP server, but accessed and reverse proxied via our nginx server.

There is no restrictions to this proxied server being on the same system either. For instance if I add this location:

location /blog {
      proxy_pass "https://bojana.dev/";
 }
bojana@7OfNine:~$ sudo nginx -s reload
bojana@7OfNine:~$ curl http://localhost:8888/blog
<!DOCTYPE html>
<html lang="en">
<head>

    <title>Bits and pieces</title>

It is clear that you can use any kind of backend to sit behind the nginx.

Here is the example with a sample node/express app as an illustration.

 location / {
     proxy_pass http://localhost:3000; # sample node app
    }
bojana@7OfNine:~/nginx/node$ node app.js
Example app listening on port 3000
bojana@7OfNine:~$ curl http://localhost:8888
Hello World!
const express = require('express')
const app = express()
const port = 3000

app.get('/', (req, res) => {
  res.send('Hello World!')
})

app.listen(port, () => {
  console.log(`Example app listening on port ${port}`)
})

I also have a sample asp.net core app running locally on Kestrel server. If I add this entry to nginx.conf:

location / {
         proxy_pass         http://localhost:5000;
}
bojana@7OfNine:~/aspnet-test$ /usr/bin/dotnet /home/bojana/aspnet-test/bin/Release/net6.0/aspnet-test.dll
info: Microsoft.Hosting.Lifetime[14]
      Now listening on: <http://localhost:5000>
info: Microsoft.Hosting.Lifetime[14]
      Now listening on: <https://localhost:5001>
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Production
info: Microsoft.Hosting.Lifetime[0]
      Content root path: /home/bojana/aspnet-test/

I can access it on localhost:8888

So we see that it doesn't really matter what sits behind your nginx proxy server, php backend, node backend, dotnet backend... You can utilize it easily as a reverse proxy.