Running Nginx as a reverse proxy in Docker is straightforward, but the best modern workflow is very different from older tutorials that modify a running container and then save it with docker commit.
A better approach is to keep the Nginx configuration in source control, build a reproducible image with a Dockerfile and use Docker Compose to place Nginx and the backend application on the same network.
This tutorial uses:
- Nginx 1.30.4 stable;
- Docker Compose;
- Apache Tomcat 11.0.24 as the backend; and
- Docker's built-in service discovery instead of hard-coded container IP addresses.
Docker Nginx reverse proxy architecture
The browser connects only to Nginx. Nginx then forwards matching requests to Tomcat over Docker's private application network.
Browser
|
| https://localhost/sample/
v
Nginx container :80
|
| https://tomcat:8080/sample/
v
Tomcat container :8080The important detail is the hostname tomcat. Docker Compose provides DNS-based service discovery, so Nginx can use the Compose service name instead of an IP address.
Project structure
Create a small project with these files:
docker-nginx-proxy/
├── compose.yaml
└── nginx/
├── Dockerfile
└── default.confThe Nginx configuration and Dockerfile are now ordinary project files that can be committed to Git, reviewed and rebuilt consistently.
Create the Nginx reverse proxy configuration
Create nginx/default.conf:
server {
listen 80;
server_name _;
location /sample/ {
proxy_pass https://tomcat:8080/sample/;
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;
}
}This configuration forwards requests under /sample/ to the Tomcat service.
The forwarded headers preserve useful information from the original request. In particular, $proxy_add_x_forwarded_for appends the client address to the forwarding chain instead of discarding an existing one.
Why service names are better than container IP addresses
Do not configure Nginx with a hard-coded address such as 192.168.246.131. Container IP addresses can change when containers are recreated.
Docker Compose creates a default network and registers each service name in Docker's internal DNS. A service named tomcat is therefore reachable at:
https://tomcat:8080This remains stable even if Docker recreates the Tomcat container with a different IP address.
Create the Nginx Dockerfile
Create nginx/Dockerfile:
FROM nginx:1.30.4
COPY default.conf /etc/nginx/conf.d/default.confThis image is reproducible. Every build starts with the same Nginx version and copies in the same configuration.
This is preferable to changing a running container and using docker commit, because the Dockerfile documents exactly how the image was produced.
Configure Nginx and Tomcat with Docker Compose
Create compose.yaml in the project root:
services:
nginx:
build:
context: ./nginx
ports:
- "80:80"
depends_on:
- tomcat
tomcat:
image: tomcat:11.0.24-jdk25-temurin
expose:
- "8080"Both services automatically join the same Compose network. Nginx can therefore connect to Tomcat through the hostname tomcat.
Notice that Tomcat uses expose, not ports. The backend does not need to publish port 8080 to the host because only Nginx needs to reach it.
Start the Docker Nginx reverse proxy
From the project directory, build and start the application:
docker compose up -d --buildCheck the running services:
docker compose psView Nginx logs if necessary:
docker compose logs nginxValidate the Nginx configuration
Before troubleshooting proxy behavior, verify that Nginx accepts the configuration:
docker compose exec nginx nginx -tA valid configuration reports that the syntax is OK and the configuration test succeeded.
Reload Nginx without rebuilding the container
During development, if you temporarily mount the configuration file instead of baking it into the image, you can test and reload Nginx directly:
docker compose exec nginx nginx -t
docker compose exec nginx nginx -s reloadFor a production image, however, rebuild and redeploy when the configuration changes so the running container matches the versioned image definition.
A development-friendly bind mount
During local development, you may prefer to mount default.conf directly so every edit does not require an image rebuild:
services:
nginx:
image: nginx:1.30.4
ports:
- "80:80"
volumes:
- ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
depends_on:
- tomcat
tomcat:
image: tomcat:11.0.24-jdk25-temurin
expose:
- "8080"The :ro suffix mounts the configuration read-only inside the container.
Test the reverse proxy
Assuming a Tomcat application is deployed at the /sample/ context path, test the Nginx endpoint:
curl -I https://localhost/sample/The request path is:
https://localhost/sample/
|
v
Nginx
|
v
https://tomcat:8080/sample/The browser never needs to know Tomcat's container address or internal port.
Understand proxy_pass trailing slashes
Trailing slashes matter in Nginx proxy configuration.
location /sample/ {
proxy_pass https://tomcat:8080/sample/;
}With that configuration, a request for:
/sample/ordersis forwarded as:
/sample/ordersIf you change either the location or proxy_pass URI, verify the resulting backend path carefully. Nginx's URI replacement rules are precise, and a misplaced slash can produce unexpected routes.
Don't disable proxy buffering by default
Proxy buffering is enabled by default and is useful for most HTTP applications. It allows Nginx to receive the upstream response efficiently while protecting the backend from slow clients.
Disable buffering only for workloads that actually require streaming or very low latency:
location /events/ {
proxy_pass https://tomcat:8080/events/;
proxy_buffering off;
}Add multiple backend services
Docker Compose and Nginx make it easy to route different paths to different containers:
server {
listen 80;
server_name _;
location /java/ {
proxy_pass https://tomcat:8080/;
}
location /api/ {
proxy_pass https://api:3000/;
}
}If the Compose file contains services named tomcat and api, Docker's internal DNS resolves both names automatically.
Use separate frontend and backend networks
For more complex applications, you can explicitly separate network access:
services:
nginx:
build: ./nginx
ports:
- "80:80"
networks:
- frontend
app:
image: tomcat:11.0.24-jdk25-temurin
networks:
- frontend
- backend
db:
image: postgres:18
networks:
- backend
networks:
frontend:
backend:
internal: trueHere, Nginx cannot communicate directly with the database because they do not share a network. The application service is the only component connected to both tiers.
Stop and remove the proxy stack
When finished, shut down the Compose project:
docker compose downThis removes the containers and project network created by Compose.
Modern Docker Nginx reverse proxy workflow
The updated workflow is intentionally simple:
default.conf
+
Dockerfile
+
compose.yaml
|
v
docker compose up --build
|
v
Nginx container
|
v
Tomcat service name
|
v
backend containerThe key improvement is reproducibility. Instead of manually editing a running container and committing its filesystem state, the Nginx configuration, image definition and application topology all live in ordinary project files.
That makes the reverse proxy easier to review, rebuild, test in CI and deploy consistently across environments.