Skip to main content

Setup Proxy Pass

A. Connect Nginx to the Node.js Server

At this point, we have two servers:

Nginx Server

Node.js Server

The missing piece is connecting them together.

Instead of sending requests directly to Node.js, we want Nginx to sit in front of the Node.js server.

To do this, we need to create a new Nginx configuration and use the proxy_pass directive.

proxy_pass allows Nginx to receive a request and forward it to another server, such as our Node.js application.

This makes Nginx act as a reverse proxy in front of the Node.js server.


B. Create a Virtual Server Configuration

Instead of modifying the large default Nginx configuration, the course creates a separate configuration for the application.

This is referred to as a:

  • virtual configuration
  • virtual server
  • virtual host

The purpose is to define how Nginx should handle requests for our application.

Conceptually:

This keeps our application configuration separate from the default Nginx configuration.


C. Create a New Nginx Configuration File

Create the configuration under Nginx's sites-available directory:

sudo vi /etc/nginx/sites-available/fsfe

Breaking it down:

/etc/nginx/
└── sites-available/
└── fsfe

Because /etc/nginx contains system configuration, we need elevated permissions:

sudo vi ...

The configuration file is named:

fsfe

which stands for the course's Full Stack Frontend application.

The filename itself is not important.

For example, it could also be named after the domain:

example.com

The filename is mainly for organization and does not determine how Nginx routes the request.


D. Why Create a Separate Configuration?

The default Nginx configuration contains many:

  • comments
  • examples
  • directives
  • settings we do not currently need

For example, a packaged default site may contain something like:

# Default server configuration
listen 80 default_server;
listen [::]:80 default_server;

root /var/www/html;
index index.html index.htm index.nginx-debian.html;
server_name _;

location / {
try_files $uri $uri/ =404;
}

# Example only: enable this after configuring TLS certificates
# listen 443 ssl default_server;

Each kind of line serves a different purpose:

KindExampleWhat it does
Comment# Default server configurationDocuments the file for people. Nginx ignores lines beginning with #.
Active settinglisten 80 default_server;Accepts HTTP traffic on port 80 and makes this the fallback virtual server.
Default static-site settingroot /var/www/html;Defines where Nginx looks for files when serving a static website.
Directivetry_files $uri $uri/ =404;Checks whether the requested file or directory exists; otherwise returns 404.
Commented example# listen 443 ssl default_server;Shows an optional HTTPS setting, but has no effect until it is uncommented and fully configured.

An example setting is therefore not automatically active. In the packaged file, examples are often commented out to show how features such as HTTPS or PHP could be configured later. Copying or uncommenting one without the required certificates, files, or services can make nginx -t fail.

Instead of modifying all of that, the course creates a smaller configuration containing only what is necessary for our server.

This makes the configuration easier to understand and maintain.

The goal is to reduce the configuration to the essential pieces required to run the application.


E. What We Are Building

The architecture we are preparing is:

Nginx becomes the public entry point instead of exposing the Node.js server directly.

The next step is to define the actual virtual server block and configure proxy_pass so Nginx knows exactly where to send the requests.

F. Create the Virtual Server Block

Now we define the actual Nginx virtual server.

A simplified configuration looks like:

server {
listen 80 default_server;
listen [::]:80 default_server;

server_name example.com;

location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}

This server block defines how Nginx should handle incoming requests for this virtual server.

Conceptually:

A virtual server does not mean another physical server.

It is simply a set of rules inside Nginx.

One physical or cloud server can therefore contain multiple Nginx virtual servers.


G. Listen on Port 80

The configuration contains:

listen 80 default_server;

Port 80 is the standard port for HTTP traffic.

Therefore:

default_server means this server block acts as the default when no other server block is a better match.

The configuration also listens on IPv6:

listen [::]:80 default_server;

So Nginx can accept HTTP requests over both IPv4 and IPv6.


H. Forward Client Information

The proxy_set_header directives pass useful information about the original request to Node.js:

proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;

This preserves the requested hostname, client-address chain, and original protocol. Applications can use these values for routing, redirects, and logging.

The root and index directives are omitted because location / proxies every matching request. In this configuration, those static-file directives would not actually provide a fallback.


I. Configure server_name

The server_name directive identifies which hostname belongs to this virtual server.

For example:

server_name example.com;

When a request arrives for:

example.com

Nginx can use the hostname to determine which virtual server configuration should handle it.

Conceptually:

For the current setup, this is less important because the server block is configured as the default server.

However, server_name becomes especially important when working with multiple domains or subdomains.

For example:


J. Configure the location Block

Next, create a location block:

location / {
...
}

The / means:

Apply these rules to requests starting from the root path.

For example:

/
├── /users
├── /products
└── /about

The location block tells Nginx how requests matching that path should be handled.


K. Use proxy_pass

Inside the location block, configure:

location / {
proxy_pass http://127.0.0.1:3000;
}

This changes Nginx's role.

Instead of serving the request itself, Nginx forwards the request to the Node.js server.

127.0.0.1 refers to the local machine.

Therefore:

127.0.0.1:3000

means:

Send the request to port 3000 on this same server.

At this point, Nginx is acting as a reverse proxy in front of the Node.js application.


L. Why Put Nginx in Front of Node.js?

A student asks why we need Nginx instead of exposing Node.js directly.

The course explains that Nginx is designed specifically for handling web traffic and provides many capabilities that would otherwise need to be handled by the application.

Examples include:

  • request proxying
  • logging
  • compression
  • SSL
  • load balancing
  • routing

Conceptually:

Rather than having every application deal with incoming internet traffic independently, Nginx can become a centralized routing layer.

This becomes increasingly useful when working with:

Multiple applications
Subdomains
Load balancers
SSL
Different backend services

The course presents this centralized routing capability as one of the major reasons for placing Nginx in front of application servers.


M. Use Only the Application's Virtual Host

A virtual host is an Nginx server {} block that defines how a particular hostname or group of requests should be handled. It is not another physical machine or another Nginx process.

One Nginx process can load multiple virtual hosts and select one by examining directives such as listen and server_name:

After installing Nginx, the relevant structure normally starts like this:

/etc/nginx/
├── nginx.conf
├── sites-available/
│ └── default
└── sites-enabled/
└── default → ../sites-available/default

sites-available stores virtual-host configurations that could be used. A file being present there does not necessarily mean it is active.

sites-enabled contains the configurations Nginx should load. On Debian and Ubuntu, entries here are commonly symbolic links to files in sites-available.

A symbolic link, or symlink, is a filesystem pointer to another file. It lets the same configuration appear under sites-enabled without creating a second copy:

/etc/nginx/sites-enabled/fsfe
↓ symlink
/etc/nginx/sites-available/fsfe
↓ actual configuration file
server { ... }

Editing /etc/nginx/sites-available/fsfe updates the configuration reached through the symlink because both paths lead to the same underlying file.

Enable the application virtual host

The course has already created the application configuration at:

/etc/nginx/sites-available/fsfe

This command enables it:

sudo ln -s /etc/nginx/sites-available/fsfe /etc/nginx/sites-enabled/fsfe

Breaking it down:

ln Create a link
-s Create a symbolic link
first path Existing configuration file
second path Symlink that Nginx loads

The result is:

/etc/nginx/sites-enabled/fsfe
→ /etc/nginx/sites-available/fsfe

This separation makes a virtual host easy to enable or disable without moving, duplicating, or deleting its real configuration file.

Disable the packaged default virtual host

The default site is still enabled through its own symlink. The course disables it with:

sudo unlink /etc/nginx/sites-enabled/default

unlink removes only the enabling symlink:

Removed:
/etc/nginx/sites-enabled/default

Still available:
/etc/nginx/sites-available/default

The course does this because the packaged default virtual host serves the default static website and declares itself as default_server. The new fsfe block also uses default_server, so leaving both enabled can produce a duplicate default-server error for port 80. Even without that conflict, it would be less obvious which configuration handles an unmatched request.

After these two commands, the structure is:

/etc/nginx/
├── sites-available/
│ ├── default # Preserved but disabled
│ └── fsfe # Application virtual host
└── sites-enabled/
└── fsfe → ../sites-available/fsfe

Conceptually:

The goal is to make the application virtual host the clearly active configuration while keeping the packaged default file available in case it is needed later.


N. Validate the Nginx Configuration

Before applying the configuration, validate it.

The course uses:

sudo nginx -t

The -t option tests the Nginx configuration.

It checks whether the configuration files are valid before we reload or restart Nginx.

The course initially runs into a permission problem because Nginx needs access to protected files such as logs.

Therefore, the command may need to be executed with sudo.

The important principle is:

Validate the Nginx configuration before applying the changes.


O. Apply the Nginx Configuration

After modifying and validating the configuration, Nginx needs to load the new settings.

Reload Nginx without dropping active connections:

sudo systemctl reload nginx

Conceptually:

After this, incoming requests can follow the newly configured proxy_pass route.


P. The Proxy Still Needs a Running Backend

After configuring Nginx, the domain begins reaching the server correctly.

However, there is still a problem:

The Node.js application has been written, but it has not actually been started.

So start it:

node app.js

Now the architecture becomes complete:

Once Node.js is running, the entire request path is connected.


Q. The Problem with Running node app.js

Running:

node app.js

works, but it creates another problem.

The Node.js process is attached to the current shell session.

If we disconnect from SSH or close the terminal:

A production application should not depend on keeping an SSH terminal open.

We need a way to keep the Node.js process running independently.


R. Use PM2 as a Process Manager

The course introduces PM2, a process manager for Node.js applications.

PM2 keeps the application running even after the terminal or SSH session is closed.

Conceptually:

With PM2:

The course installs PM2 globally with npm.

sudo npm install -g pm2

A global installation makes the pm2 command available system-wide.


S. Start the Application with PM2

Instead of:

node app.js

start the application through PM2:

pm2 start app.js

PM2 now manages the Node.js process.

You can inspect the managed processes with:

pm2 list

Conceptually:

Even if the SSH shell is closed, PM2 keeps the application process alive.


T. Make the Application Survive Server Reboots

Keeping the application alive after closing SSH is useful, but there is another case to handle:

We want the application to automatically return after the machine restarts.

The course uses:

pm2 save

This saves the current PM2 process list.

Then:

pm2 startup

PM2 generates a command that configures the operating system to start PM2 during system startup.

The course instructs us to copy and execute the generated command.

Conceptually:

Now the application can come back online automatically after a server reboot.

To verify the startup configuration, reboot the server, reconnect over SSH, and inspect the restored process:

sudo reboot
# Reconnect after the server comes back online
pm2 list
curl http://127.0.0.1:3000

The process should appear as online, and the local request should return the application response.


U. PM2 Can Restart Crashed Applications

PM2 can also help when the Node.js application crashes.

Without a process manager:

With PM2:

This improves application availability.

However, automatic restarts introduce another concern.

If the application constantly crashes:

the application may appear to stay online while an underlying problem remains.

Therefore, we should still monitor:

  • application logs
  • crashes
  • restart frequency
  • errors

The course notes that logs should be inspected or sent somewhere visible so repeated crashes are not hidden by PM2's automatic recovery.

Inspect the process table, logs, and restart count with:

pm2 list
pm2 logs app

Press Ctrl+C to stop following the logs; this does not stop the managed application.


V. PM2 Watch Mode

PM2 can also watch files for changes.

When watch mode is enabled:

This is similar in concept to development tools that automatically restart a Node.js server when source files change.

Enable it when starting the process:

pm2 start app.js --watch

Watch mode is mainly useful during development. On a production server, deploy deliberately and restart the process explicitly so incidental file changes do not trigger unexpected restarts.


W. Complete Request Flow

At the end of this setup, the complete architecture is:

PM2 handles the lifecycle of the Node.js process:

PM2
├── Keep app running after SSH disconnect
├── Restart app after crashes
└── Restore app after server reboot

Nginx handles incoming web traffic:

Nginx
├── Receive HTTP requests
├── Route requests
├── Proxy requests to Node.js
└── Provide infrastructure capabilities

The result is a complete path from the public internet to the application:

The server was built and configured from the ground up, and the application can now remain running without requiring an open SSH terminal.