Skip to main content

Module 06 — Nginx Reverse Proxy

Objective

Install Nginx on web-server and configure it as a reverse proxy to the Go backend running on app-server. By the end of this module, users will access the application through Nginx on port 80 instead of connecting directly to the Go app on port 8080.

Prerequisites

  • Module 05 complete — login system tested and working
  • Go app running on app-server at http://192.168.56.12:8080
  • SSH access to web-server (192.168.56.13)

1. Install Nginx

SSH into web-server and install Nginx:

ssh web-server
sudo apt update && sudo apt install -y nginx

Nginx starts automatically after installation. Verify it is running:

sudo systemctl status nginx

You should see active (running) in the output.

Verify from your Mac:

curl http://192.168.56.13

This should return the Nginx default welcome page HTML, confirming that Nginx is listening on port 80.

Checkpoint: You have a working Nginx installation on web-server serving the default welcome page on port 80.


2. Understand Reverse Proxying

A reverse proxy sits between clients and backend servers. Instead of clients connecting directly to the application, they connect to the reverse proxy, which forwards the request to the backend and returns the response.

The request flow with Nginx:

Client (Mac browser)
|
| HTTP request to port 80
v
Nginx (web-server 192.168.56.13:80)
|
| Proxied request to port 8080
v
Go app (app-server 192.168.56.12:8080)
|
| Query
v
PostgreSQL (db-server 192.168.56.11:5432)

Why use a reverse proxy?

  • Single entry point — clients only need to know one address (the Nginx server), not the addresses of every backend service
  • SSL termination — Nginx can handle HTTPS, so the Go app does not need to deal with certificates (we will set this up in Phase 2)
  • Load balancing — Nginx can distribute traffic across multiple backend instances (useful when scaling)
  • Static file caching — Nginx can cache static assets and serve them directly, reducing load on the backend

For now, we are using the simplest configuration: one Nginx instance proxying to one backend.


3. Configure Nginx as a Reverse Proxy

Remove the default site

The default Nginx configuration serves the welcome page. Remove it:

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

Create the reverse proxy configuration

Create a new configuration file for the customer app:

sudo nano /etc/nginx/sites-available/customerapp

Add the following content:

server {
listen 80;
server_name _;

location / {
proxy_pass http://192.168.56.12:8080;
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;
}
}

What each line does:

  • listen 80 — accept connections on port 80 (HTTP)
  • server_name _ — match any hostname (we do not have a domain name yet)
  • proxy_pass http://192.168.56.12:8080 — forward all requests to the Go app on app-server
  • proxy_set_header Host $host — pass the original Host header to the backend
  • proxy_set_header X-Real-IP $remote_addr — tell the backend the real client IP address
  • proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for — append to the forwarded-for chain so the backend knows the request was proxied
  • proxy_set_header X-Forwarded-Proto $scheme — tell the backend whether the original request was HTTP or HTTPS

Enable the site

Create a symbolic link from sites-available to sites-enabled:

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

Test the configuration

Always test Nginx configuration before reloading:

sudo nginx -t

You should see:

nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

Reload Nginx

Apply the new configuration without downtime:

sudo systemctl reload nginx

Checkpoint: Nginx is configured to proxy all requests on port 80 to the Go app on app-server:8080.


4. Test Through Nginx

Test the health endpoint

From your Mac:

curl http://192.168.56.13/health

This should return the health check response from the Go app, but the request went through Nginx on web-server before reaching app-server.

Test in the browser

Open http://192.168.56.13 in your Mac browser. You should see the login page — the same one you previously accessed at http://192.168.56.12:8080, but now served through Nginx.

Log in, create a customer, edit it, and delete it. All CRUD operations should work through Nginx.

Verify direct access still works

The Go app is still directly accessible:

curl http://192.168.56.12:8080/health

Both paths work. You now have two ways to reach the application:

| Path | URL | Route | |-|-| | Direct | http://192.168.56.12:8080 | Mac -> Go app | | Through Nginx | http://192.168.56.13 | Mac -> Nginx -> Go app |

Checkpoint: The application is fully functional through both the direct path and the Nginx reverse proxy.


5. Understanding What Changed

Before this module, the only way to access the app was directly on http://192.168.56.12:8080. Now there are two paths to the same application.

Why this matters for the next steps:

  • In Module 07, we will expose only the Nginx path to the internet using port forwarding. The direct path on port 8080 will remain internal-only.
  • This means Nginx becomes the single entry point — all external traffic flows through it.
  • Later in Phase 2, we will add SSL (HTTPS) at the Nginx layer, so the Go app never needs to handle certificates.

About the proxy_set_header lines:

When Nginx forwards a request to the Go app, the Go app sees the connection coming from Nginx's IP (192.168.56.13), not the original client. The proxy_set_header directives preserve the original client information:

  • X-Real-IP contains the actual client IP address
  • X-Forwarded-For contains the chain of proxies the request passed through
  • X-Forwarded-Proto tells the backend whether the client used HTTP or HTTPS

Without these headers, the Go app would think every request comes from the Nginx server. This matters for logging, rate limiting, and security decisions.

About CORS:

No CORS (Cross-Origin Resource Sharing) headers are configured in this module. This is intentional. Since the browser loads the page from Nginx and the API requests also go through the same Nginx address, there is no cross-origin situation. Phase 2 will cover CORS when we introduce separate domains.

About HTTPS:

This module uses HTTP only — no SSL/TLS. This is also intentional. Setting up SSL requires a domain name and certificates, which we will cover in Phase 2.


Troubleshooting

502 Bad Gateway

Nginx reached the backend but got no valid response. The Go app is probably not running on app-server.

ssh app-server "sudo systemctl status customerapp"

If the service is not running, start it:

ssh app-server "sudo systemctl start customerapp"

Also verify the IP and port in your Nginx config match where the Go app is actually listening.

Nginx config test fails

Run sudo nginx -t and read the error message carefully. Common causes:

  • Missing semicolon at the end of a line
  • Mismatched braces
  • Typo in a directive name

Connection refused on port 80

Nginx is not running:

sudo systemctl start nginx
sudo systemctl status nginx

Check for errors in the Nginx logs:

sudo tail -20 /var/log/nginx/error.log

Still seeing the default Nginx welcome page

Either you did not remove the default site or you did not reload Nginx:

# Verify default is removed
ls /etc/nginx/sites-enabled/
# Should only show: customerapp

# Verify your config is linked
ls -la /etc/nginx/sites-enabled/customerapp
# Should be a symlink to /etc/nginx/sites-available/customerapp

# Reload Nginx
sudo systemctl reload nginx

App works directly but not through Nginx

Check that app-server is reachable from web-server:

# From web-server
curl http://192.168.56.12:8080/health

If this fails, there may be a network or firewall issue between the two VMs.