Reverse Proxy with Nginx and Go for Microservices
As an application grows, splitting it into smaller services can make independent deployment and scaling easier. For example:
- Product service on port 8080
- Blog service on port 8081
Users should not need to know those internal ports. An Nginx reverse proxy can expose both services under one domain and route requests by URL path.
1. Configure the Nginx Reverse Proxy
Create a site configuration:
sudo nano /etc/nginx/sites-available/yourdomain.comAdd:
upstream api_product {
server 127.0.0.1:8080;
}
upstream api_blog {
server 127.0.0.1:8081;
}
server {
listen 80;
server_name yourdomain.com;
# Proxy to the product service
location /product/ {
proxy_pass http://api_product/;
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 to the blog service
location /blog/ {
proxy_pass http://api_blog/;
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;
}
}Enable the configuration:
sudo ln -s /etc/nginx/sites-available/yourdomain.com /etc/nginx/sites-enabled/Validate it:
sudo nginx -tThen reload Nginx:
sudo systemctl reload nginx2. Product Service
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
type Response struct {
Message string `json:"message"`
Data []Product `json:"data,omitempty"`
}
type Product struct {
Name string `json:"name"`
Price int `json:"price"`
}
func main() {
r := gin.Default()
r.GET("/", func(c *gin.Context) {
c.JSON(http.StatusOK, Response{
Message: "Success",
Data: []Product{
{Name: "Product 1", Price: 100},
{Name: "Product 2", Price: 200},
{Name: "Product 3", Price: 300},
},
})
})
r.POST("/create", func(c *gin.Context) {
var product Product
if err := c.ShouldBindJSON(&product); err != nil {
c.JSON(http.StatusBadRequest, Response{Message: "Bad Request"})
return
}
c.JSON(http.StatusCreated, Response{
Message: "Product Created",
Data: []Product{product},
})
})
r.Run(":8080")
}3. Blog Service
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
type Response struct {
Message string `json:"message"`
Data []Article `json:"data,omitempty"`
}
type Article struct {
Name string `json:"name" binding:"required"`
Content string `json:"content" binding:"required"`
}
func main() {
r := gin.Default()
r.GET("/", func(c *gin.Context) {
c.JSON(http.StatusOK, Response{
Message: "Success",
Data: []Article{
{Name: "Article 1", Content: "Content 1"},
{Name: "Article 2", Content: "Content 2"},
{Name: "Article 3", Content: "Content 3"},
},
})
})
r.POST("/create", func(c *gin.Context) {
var article Article
if err := c.ShouldBindJSON(&article); err != nil {
c.JSON(http.StatusBadRequest, Response{Message: "Bad Request"})
return
}
c.JSON(http.StatusCreated, Response{
Message: "Article Created",
Data: []Article{article},
})
})
r.Run(":8081")
}4. Access the Services
Once both services and Nginx are running:
http://yourdomain.com/product/→ routes to the product service on port 8080.http://yourdomain.com/blog/→ routes to the blog service on port 8081.
Clients use a single public domain and do not need to know the internal service ports.
5. Conclusion
With separate sites-available and sites-enabled configuration, Nginx can provide a clean routing layer for multiple microservices. Combined with lightweight Go services, this pattern gives you a simple foundation that can later be extended with TLS, health checks, load balancing, observability, and container orchestration as the system grows.