Debugging a Stubborn nginx Docker Container: A Deep Dive into Port Conflicts, SSL Certificates, and Health Checks

Debugging a Stubborn nginx Docker Container: A Deep Dive into Port Conflicts, SSL Certificates, and Health Checks

The Challenge

I recently faced a perplexing issue where my nginx container refused to start in a Docker Compose stack. What initially seemed like a simple container startup problem turned into an extensive troubleshooting journey through multiple layers of containerization complexity. My application stack included PostgreSQL, Redis, a Rails backend, a React frontend, and Sidekiq workers - all running smoothly except for the nginx reverse proxy that tied everything together.

Issue #1: Windows Port Reservation Blocking Container Startup

The Discovery

My first attempt to start the nginx container resulted in a cryptic error:

Error response from daemon: ports are not available: exposing port TCP 0.0.0.0:80 -> 127.0.0.1:0: 
listen tcp 0.0.0.0:80: bind: An attempt was made to access a socket in a way forbidden by its access permissions.

Investigation Commands

# Check if ports 80 and 443 are in use
sudo netstat -tlnp | grep -E ':80|:443'

# Test if alternative ports work
docker run -d --name nginx-test -p 8080:80 -p 8443:443 nginx:alpine

# Verify the test container is running
docker ps | grep nginx-test

The Root Cause

I discovered that Windows reserves certain ports at the kernel level, even when they appear unoccupied. This is a Windows-specific issue where the HTTP.sys service claims ports 80 and 443, preventing Docker containers in WSL2 from binding to them.

The Solution

I created a Windows-specific Docker Compose configuration using alternative ports:

# Create a Windows-specific compose file
cp docker-compose.prod.yml docker-compose.prod-windows.yml

# Modify port mappings to use 8080 and 8443
sed -i 's/"80:80"/"8080:80"/g; s/"443:443"/"8443:443"/g' docker-compose.prod-windows.yml

Issue #2: Volume Mount Type Mismatch

The Discovery

After resolving the port issue, I encountered another error:

Error mounting "/nginx/nginx.prod.conf" to rootfs: 
Are you trying to mount a directory onto a file (or vice-versa)?

Investigation Commands

# Check if the nginx configuration file exists
ls -la ./nginx/nginx.prod.conf

# Discovered it was actually a directory!
# Output showed: drwxr-xr-x (directory) instead of -rw-r--r-- (file)

# Check directory contents
sudo ls -la ./nginx/nginx.prod.conf/

The Root Cause

Someone had accidentally created a directory named nginx.prod.conf instead of a configuration file. Docker's volume mounting system strictly enforces type matching - I couldn't mount a directory where a file was expected.

The Solution

# Remove the empty directory
sudo rm -r ./nginx/nginx.prod.conf

# Copy the existing config file to the expected name
cp ./nginx/nginx.conf ./nginx/nginx.prod.conf

Issue #3: Missing SSL Certificates Causing Restart Loop

The Discovery

The container started but immediately entered a restart loop with this error:

nginx: [emerg] cannot load certificate "/etc/nginx/ssl/fullchain.pem": 
BIO_new_file() failed (SSL: error:80000002:system library::No such file or directory)

Investigation Commands

# Check container logs
docker logs pcvn-erp-nginx-prod

# Examine SSL directory contents
ls -la ./nginx/ssl/
# Output: empty directory

# Check SSL configuration in nginx
grep -n "ssl\|443\|fullchain\|privkey" ./nginx/nginx.prod.conf

The Root Cause

The nginx configuration expected SSL certificates for HTTPS, but the ssl directory was empty. The configuration required three files: fullchain.pem, privkey.pem, and chain.pem.

The Solution

I generated self-signed certificates for local development:

# Generate self-signed certificate and key
sudo openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
  -keyout ./nginx/ssl/privkey.pem \
  -out ./nginx/ssl/fullchain.pem \
  -subj "/C=US/ST=State/L=City/O=Development/CN=localhost"

# Create chain file for nginx SSL stapling
sudo cp ./nginx/ssl/fullchain.pem ./nginx/ssl/chain.pem

Issue #4: Health Check Failing Due to Port Mismatch

The Discovery

The container was running but marked as "unhealthy". Docker health check logs revealed:

wget: bad address 'erp.pcvn.com'

Investigation Commands

# Check health check status
docker inspect pcvn-erp-nginx-prod --format='{{json .State.Health}}' | python3 -m json.tool

# Find server block configurations
grep -n "listen\|server_name" ./nginx/nginx.prod.conf

# Discovered localhost server block listening on port 8080, not 80!
# Line 273: listen 8080;
# Line 275: server_name localhost;

# Check health check configuration
grep -B 2 -A 4 "test:" docker-compose.prod-windows.yml

The Root Cause

The health check was configured to test http://localhost/health (port 80), but the localhost server block in nginx was listening on port 8080. When the health check connected to port 80, nginx couldn't find a matching server block for localhost and fell back to the default server block for erp.pcvn.com, which tried to redirect to a non-existent domain.

The Solution

First, I added a health endpoint to the nginx configuration:

# Add health endpoint to localhost server block
sudo sed -i '284a\
\
        location /health {\
            access_log off;\
            return 200 "OK";\
            add_header Content-Type text/plain;\
        }' ./nginx/nginx.prod.conf

Then I corrected the health check URL to use the right port:

# Fix health check to use port 8080
sed -i 's|"http://localhost/health"|"http://localhost:8080/health"|' docker-compose.prod-windows.yml

# Recreate container with updated health check
docker-compose -f docker-compose.prod-windows.yml up -d --force-recreate nginx

Key Lessons Learned

1. Windows Port Reservations in WSL2

I learned that Windows reserves certain ports at the kernel level, which affects Docker containers running in WSL2. These ports appear free in Linux but are blocked at the Windows layer.

2. Docker Volume Mount Type Enforcement

Docker strictly enforces that volume mount sources and destinations must be the same type (both files or both directories). This prevented my container from starting when a directory existed where a file was expected.

3. SSL Certificate Requirements

Even in development, nginx won't start without the SSL certificates specified in its configuration. Self-signed certificates work perfectly for local development.

4. Health Check Port Accuracy

Health checks must target the exact port where services are listening. A mismatch between the health check URL and the actual listening port caused my container to appear unhealthy despite running correctly.

5. Container Recreation vs. Restart

When changing container configurations like health checks, I needed to recreate the container entirely rather than just restarting it, as these settings are baked into the container's metadata at creation time.

The Final Working Architecture

After resolving all issues, my stack now runs successfully with:

  • nginx: Reverse proxy on ports 8080 (HTTP) and 8443 (HTTPS)
  • PostgreSQL: Database on port 5432
  • Redis: Cache on port 6379
  • Rails Backend: API on port 3002
  • React Frontend: UI on port 3003
  • Sidekiq: Background job processor

The creation of a Windows-specific Docker Compose configuration file allows my development environment to work around Windows port reservations while maintaining the original configuration for deployment to Linux servers.

Conclusion

This debugging journey reinforced for me the importance of systematic troubleshooting in containerized environments. Each issue I resolved uncovered the next layer of problems, requiring patience and methodical investigation. Through the process, I deepened my understanding of how Docker networking, volume mounting, and health checks interact across different operating system layers, particularly in the complex WSL2 environment on Windows.

The final success came not just from fixing each issue, but from understanding why each problem occurred in the first place. By approaching errors as learning opportunities, I built the expertise I need to handle similar challenges in future projects.


If you enjoyed this article, you can also find it published on LinkedIn and Medium.