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-serverproxy_set_header Host $host— pass the original Host header to the backendproxy_set_header X-Real-IP $remote_addr— tell the backend the real client IP addressproxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for— append to the forwarded-for chain so the backend knows the request was proxiedproxy_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-IPcontains the actual client IP addressX-Forwarded-Forcontains the chain of proxies the request passed throughX-Forwarded-Prototells 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.