Configuring Nginx for WebSocket and HTTP Coexistence in Docker Environments
Initial Configuration Challenges
A recent project required serving both HTTP and WebSocket traffic through a single nginx configuration file. The initial setup was as follows:
upstream backend_service {
server 10.6.14.200:8000 max_fails=0;
}
server {
listen 80;
gzip on;
gzip_min_length 1k;
gzip_comp_level 9;
gzip_types text/plain text/css text/javascript application/json application/javascript application/x-javascript application/xml;
gzip_vary on;
gzip_disable "MSIE [1-6]\.";
root /usr/share/nginx/html;
location / {
try_files $uri $uri/ /index.html;
}
location /websocket {
proxy_pass http://backend_service;
proxy_http_version 1.1;
proxy_read_timeout 360s;
proxy_redirect off;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host:$server_port;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header REMOTE-HOST $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
While HTTP requests functioned correctly, WebSocket connections consistently failed to establish.
Enhanced Configuration Supporting Both Protocols
According to nginx documentation on WebSocket handling, custom variables can be defined using the map directive. The improved configuration below supports both WebSocket and HTTP requests:
upstream backend_service {
server 10.6.14.200:8000 max_fails=0;
}
map $http_upgrade $connection_protocol {
default keep-alive;
'websocket' upgrade;
}
server {
listen 80;
gzip on;
gzip_min_length 1k;
gzip_comp_level 9;
gzip_types text/plain text/css text/javascript application/json application/javascript application/x-javascript application/xml;
gzip_vary on;
gzip_disable "MSIE [1-6]\.";
root /usr/share/nginx/html;
location / {
try_files $uri $uri/ /index.html;
}
location /websocket {
proxy_pass http://backend_service;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_protocol;
}
}
Dynamic Backend Configuration with Environment Variables
Nginx does not native support environment variables in configuration files. However, envsubst provides a viable workaround for dynamic configuration generation:
# nginx.conf.template
upstream backend_service {
server ${BACKEND_HOST}:${BACKEND_PORT} max_fails=0;
}
map $http_upgrade $connection_protocol {
default keep-alive;
'websocket' upgrade;
}
server {
listen 80;
gzip on;
gzip_min_length 1k;
gzip_comp_level 9;
gzip_types text/plain text/css text/javascript application/json application/javascript application/x-javascript application/xml;
gzip_vary on;
gzip_disable "MSIE [1-6]\.";
root /usr/share/nginx/html;
location / {
try_files $uri $uri/ /index.html;
}
location /websocket {
proxy_pass http://backend_service;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_protocol;
}
}
Project directory structure:
├── dist
├── docker-entrypoint.sh
├── Dockerfile
└── nginx.conf.template
Antrypoint script for dynamic configuration:
#!/bin/bash
set -eu
envsubst '${BACKEND_HOST} ${BACKEND_PORT}' < /etc/nginx/conf.d/nginx.conf.template > /etc/nginx/conf.d/default.conf
exec nginx -g "daemon off;"
Dockerfile definition:
FROM nginx:alpine
COPY nginx.conf.template /etc/nginx/conf.d/nginx.conf.template
COPY docker-entrypoint.sh /docker-entrypoint.sh
COPY dist /usr/share/nginx/html
RUN chmod +x /docker-entrypoint.sh
EXPOSE 80
ENTRYPOINT ["/docker-entrypoint.sh"]
Build command:
docker build -t web-app:latest .
Run commend with environment variables:
docker run -d \
--restart=always \
-p 10086:80 \
-e BACKEND_HOST=10.6.14.200 \
-e BACKEND_PORT=8000 \
--name web-container \
web-app:latest
This approach works well for single-container deployments. For orchestrated environments like Docker Compose or Kubernetes, internal service discovery mechanisms provide more streamlined connectivity without requiring these manual steps.