The Reverse Proxy Pattern
You wrote a Go web server. It listens on port 8080. You run curl localhost:8080 and see your JSON response. It works. Now you want to put this on a server and access it via example.com. You could bind Go to port 80, but that requires root privileges and exposes your application directly to the internet. A better approach is to place Nginx in front of your Go app. Nginx handles the public port, SSL termination, and static assets. Go focuses on business logic. This setup is called a reverse proxy.
What a reverse proxy actually does
A reverse proxy accepts requests from clients and forwards them to backend servers. The client never talks directly to the backend. Nginx acts as the intermediary. This separation provides several benefits. You can terminate TLS at Nginx so Go doesn't manage certificates. You can serve static files like CSS and images with Nginx, which is optimized for this task. You can buffer responses to protect Go from slow clients. You can load balance across multiple Go instances.
Think of a hotel. The front desk is Nginx. The guest rooms are your Go app. Guests check in at the front desk. They don't walk into the rooms unannounced. The front desk handles the key, the luggage, and the complaints. When a guest needs something, the front desk communicates with the room. The room focuses on being comfortable. The front desk protects the privacy of the rooms and manages the flow of people.
Nginx is the shield. Go is the engine.
Minimal configuration
Here's the bare minimum Nginx config to forward traffic to a Go app on port 8080. This block listens on port 80, matches the domain, and proxies requests to localhost. It also sets essential headers so Go can see the original client information.
server {
listen 80;
server_name example.com;
location / {
proxy_pass http://127.0.0.1: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;
}
}
listen 80 binds Nginx to the standard HTTP port. server_name ensures this block only handles requests for example.com. proxy_pass sends the request to the Go app. Host preserves the domain name so Go knows which virtual host is being requested. X-Real-IP passes the client's IP address. X-Forwarded-For builds a chain of proxy IPs. X-Forwarded-Proto tells Go whether the original request was HTTP or HTTPS.
Request flow and headers
When a client requests example.com, the packet hits Nginx first. Nginx checks the server_name. If it matches, Nginx enters the location / block. Nginx creates a new connection to 127.0.0.1:8080. Before forwarding, Nginx modifies the headers. It sets Host to example.com. It adds X-Real-IP with the client's IP. It appends the client IP to X-Forwarded-For.
Go receives the request. Go sees the connection coming from 127.0.0.1. If you log r.RemoteAddr without reading headers, you only see 127.0.0.1. You must read X-Real-IP to get the actual client IP. Go processes the request and sends a response. Nginx receives the response and forwards it to the client.
Headers are the contract. Break the contract and the app lies to itself.
Production-ready configuration
Real deployments need more than forwarding. You need timeouts to prevent hanging connections. You need buffering to protect Go from slow clients. You need HTTP/1.1 support for keep-alive and WebSockets. Here's a config that handles these requirements.
server {
listen 80;
server_name example.com;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
proxy_set_header Connection "";
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;
proxy_connect_timeout 5s;
proxy_read_timeout 60s;
proxy_send_timeout 60s;
proxy_buffering on;
proxy_buffer_size 4k;
proxy_buffers 8 4k;
}
}
proxy_http_version 1.1 enables persistent connections and WebSocket upgrades. Connection "" removes the default Connection: close header, allowing keep-alive. Timeouts define how long Nginx waits for the backend. proxy_buffering on tells Nginx to read the response from Go into memory before sending it to the client. This decouples the client speed from Go's goroutine usage. If a client has slow internet, Go dumps the response to Nginx and frees the goroutine immediately. Nginx waits for the client.
Buffering protects goroutines. Let Nginx wait for slow clients.
Go side: reading headers and binding
Go needs to read the headers Nginx sets. Go should also bind to localhost when behind a proxy. This ensures no external traffic reaches the app directly. The proxy is the sole entry point.
package main
import (
"net/http"
)
// Handler demonstrates reading proxy headers.
func handler(w http.ResponseWriter, r *http.Request) {
// X-Real-IP contains the original client IP.
clientIP := r.Header.Get("X-Real-IP")
if clientIP == "" {
// Fallback to RemoteAddr if headers are missing.
clientIP = r.RemoteAddr
}
// X-Forwarded-Proto indicates the original scheme.
scheme := r.Header.Get("X-Forwarded-Proto")
if scheme == "" {
scheme = "http"
}
w.Header().Set("Content-Type", "text/plain")
w.Write([]byte("IP: " + clientIP + ", Scheme: " + scheme))
}
func main() {
// Bind to localhost only when behind a proxy.
// This prevents direct external access to the app.
http.HandleFunc("/", handler)
http.ListenAndServe("127.0.0.1:8080", nil)
}
r.Header.Get retrieves the header value. The fallback logic handles direct requests during development. ListenAndServe binds to 127.0.0.1, restricting access to the loopback interface. Go compiles to a single static binary. Deployment is copying the file to the server and running it. No runtime dependencies. No package manager on the server. This makes Go apps trivial to deploy behind Nginx.
Bind to localhost. Expose the proxy, not the app.
Common pitfalls
The trailing slash in proxy_pass changes how Nginx rewrites the URI. If you define location /api/ and use proxy_pass http://backend, Nginx forwards the full path /api/users to the backend. If you use proxy_pass http://backend/, Nginx strips the /api/ prefix and forwards /users. This mismatch causes 404 errors when the backend expects the full path. Test the path rewriting carefully.
WebSockets require header upgrades. Nginx defaults to HTTP/1.0 for proxy connections, which breaks WebSocket handshakes. You must set proxy_http_version 1.1 and pass the Upgrade and Connection headers. Without this, WebSocket connections fail with a 502 error.
If you mistype a directive, Nginx rejects the config with unknown directive during nginx -t. Fix the typo before reloading. Never reload Nginx without testing the configuration first.
Test with nginx -t. Never reload without testing.
When to use Nginx versus alternatives
Use Nginx when you need fine-grained control over buffering, caching, and complex routing rules. Use Caddy when you want automatic HTTPS and simpler configuration without manual certificate management. Use Go directly when you are deploying to a platform that handles ingress for you, like Kubernetes Ingress or Cloudflare Workers. Use a load balancer when you have multiple Go instances and need to distribute traffic across them.