A Reverse Proxy is an intermediary server placed at the edge of your network to receive all user requests and distribute them among your containers.
Imagine you’ve just rented your VPS server. You want to deploy your first application (a React web app) and expose it on port 80. Everything works perfectly.
But the next day you decide to add a second application: a Node.js API. You go to your docker-compose.yml, tell it to also expose port 80 so the public can access it, run the command, and… BAM! Error:
Bind for 0.0.0.0:80 failed: port is already allocated
Port 80 (the internet standard) is like the main door to your server. Physically, only one program can hold the key to that door at a time. If your web app is already using it, your API is locked out.
The hacky solution is to put the API on port 8080 and force your users to type my-api.com:8080. The reasonable solution is to use Nginx.
Today we’ll learn how to configure this classic web server as a native Reverse Proxy in Docker to handle multiple domains on a single machine 👇.
The Architecture: Nginx to the Rescue
To solve the port collision, we’ll completely change our network topology.
Instead of our applications facing the internet, we’re going to hide them. None of your applications (neither the web app nor the API) will expose ports to the outside world.
Instead, we’ll create a new container dedicated exclusively to Nginx. This container will be the only one exposing port 80.
Its job will be to listen to all requests, read the domain the user typed in their browser (e.g., api.mydomain.com), and route the traffic internally to the corresponding container.
Why do you need a Reverse Proxy?
- Eliminate port collisions: You can have 50 different applications on your server, all responding on port 80.
- Perimeter security: Your applications are isolated from the public internet. They only receive traffic filtered through Nginx.
- Single entry point: You have a single entrance, which makes it easier to implement caching, load balancing, or security certificates in the future.
The docker-compose.yml File
Let’s see how this translates into code. We’ll create an environment with three containers: the Web app, the API, and our Nginx gatekeeper.
services:
# 1. Our main web app (Hidden)
my_web:
image: nginx:alpine
# We don't use "ports". It only exists on the internal network.
# 2. Our API (Hidden)
my_api:
image: node:18-alpine
command: node index.js
# We don't use "ports" either.
# 3. The Reverse Proxy (The only visible one)
proxy:
image: nginx:alpine
ports:
- "80:80" # The only container that talks to the internet
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro # We inject the configuration
depends_on:
- my_web
- my_api
Notice the key detail: my_web and my_api do not have the ports directive. Only proxy does.
Configuring the Routing
Now we need to tell Nginx how to distribute the traffic. To do this, we mounted a volume that injects our nginx.conf file into the proxy container.
We’ll configure it using server blocks (what is classically known as Virtual Hosts):
events {}
http {
# Rule 1: If someone accesses web.mydomain.com
server {
listen 80;
server_name web.mydomain.com;
location / {
# Redirect traffic to the "my_web" container on internal port 80
proxy_pass http://my_web:80;
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;
}
}
# Rule 2: If someone accesses api.mydomain.com
server {
listen 80;
server_name api.mydomain.com;
location / {
# Redirect traffic to the "my_api" container on internal port 3000
proxy_pass http://my_api: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;
}
}
}
Analyzing the configuration:
server_name: Nginx reads the HTTP header of the request to know which domain the user typed.proxy_pass: This is the star directive. It tells Nginx: “take this request and send it to this other location”. As you can see, we directly use the container names (http://my_webandhttp://my_api) thanks to Docker’s internal DNS.proxy_set_header: Ensures that Nginx passes the original user information (like their IP or the requested domain) to the final container, so your application knows who is actually visiting it.
