Using Nginx Reverse Proxy to Enable External Access to Containers

In containerized deployments, applications typically run inside Docker containers. Mapping each container's port directly to the host often leads to port management chaos, inability to uniformly configure HTTPS, and difficulty centrally controlling access policies. By leveraging Nginx as a reverse proxy, external clients can access multiple backend container services through a unified port 80 or 443, while gaining capabilities such as load balancing, caching, and centralized logging. This article explores typical implementation patterns for exposing containerized services externally via Nginx reverse proxy.

Basic Reverse Proxy Configuration

Assume Nginx is installed on the host and a Web container is running with its port 80 mapped to the host's 8080:

docker run -d --name webapp -p 8080:80 my-webapp:latest

At this point, http://<host-IP>:8080 can already access the service. The goal, however, is to allow users to access the service directly via port 80 using a domain name.

Edit the Nginx configuration file (e.g., /etc/nginx/conf.d/webapp.conf) and add the following:

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;
    }
}

The directive roles are as follows:

  • proxy_pass: Specifies the backend address, i.e., the port mapped on the host.
  • Host: Passes the original domain name to the backend, preventing the application from generating incorrect redirect addresses.
  • X-Real-IP and X-Forwarded-For: Forward the real client IP to the containerized application; otherwise, the application always sees the Nginx address.
  • X-Forwarded-Proto: Informs the backend whether the original request was HTTP or HTTPS, enabling the application to generate correct links.

Multi-Container Routing Strategies

When multiple containers run on a single host, two common routing approaches exist.

Path-Based Routing

location /app1/ {
    proxy_pass http://127.0.0.1:8081/;
}
location /app2/ {
    proxy_pass http://127.0.0.1:8082/;
}

Note the detail of the trailing slash after proxy_pass: when a slash is present, Nginx strips the matched location prefix before forwarding; without a slash, the full path is forwarded. Misconfiguration here often results in 404 errors.

Domain-Based Routing

Create independent server blocks, each using a distinct server_name:

server {
    listen 80;
    server_name app1.example.com;
    location / {
        proxy_pass http://127.0.0.1:8081;
    }
}
server {
    listen 80;
    server_name app2.example.com;
    location / {
        proxy_pass http://127.0.0.1:8082;
    }
}

This approach minimizes application invasiveness and is recommended for production environments.

Deploying Nginx in a Container with Reverse Proxy

If Nginx itself runs inside a container, there is no need to map backend container ports to the host; Docker network internal communication suffices:

docker network create proxy-net
docker run -d --name webapp --network proxy-net my-webapp:latest
docker run -d --name nginx --network proxy-net -p 80:80 \
    -v /opt/nginx/conf.d:/etc/nginx/conf.d nginx:latest

In the configuration, the container name can be used directly as a hostname (Docker's built-in DNS resolves it automatically):

location / {
    proxy_pass http://webapp:80;
}

This method completely avoids exposing backend ports externally, improving security.

WebSocket Support

If the containerized application uses WebSocket, additional upgrade headers must be added; otherwise, the connection will be interrupted:

location /ws {
    proxy_pass http://127.0.0.1:8080;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_read_timeout 3600s;
}

The proxy_read_timeout directive helps avoid long connections being prematurely terminated by Nginx due to idle timeout.

Configuration Validation and Testing

After modifying the configuration, check syntax and reload:

nginx -t
nginx -s reload

Verify the proxy is working:

curl -I http://example.com

A 200 status code or the application's expected response indicates successful proxying. Additionally, inspect logs inside the container to confirm requests carry the real client IP.

Common Troubleshooting

  • 502 Bad Gateway: The backend container is unreachable. Check if the container is running, port mapping is correct, and the host firewall allows the connection. On CentOS, SELinux might block Nginx from initiating network connections; running setsebool -P httpd_can_network_connect 1 can resolve this.
  • Backend always sees 127.0.0.1: Ensure X-Real-IP and related headers are configured, and the application correctly reads X-Forwarded-For.
  • Page resources or APIs return 404: Often caused by incorrect interaction between the trailing slash of proxy_pass and the prefix of location, leading to path concatenation errors.

Summary

Nginx reverse proxy is the standard entry-solution for containerized services: when deployed on a host, proxying is achieved via port mapping; when Nginx runs in a container, backend services are accessed directly via Docker networking using container names. Combined with request-header forwarding, path or domain-based routing, and WebSocket support, multiple container services can be safely and flexibly exposed behind a single entry point, laying the groundwork for subsequent HTTPS certificate deployment and load balancing.