How to Hide Multiple Docker Apps Behind a Single Nginx Gateway
This guide shows how to containerize five services, expose only ports 80/443 via an Nginx reverse‑proxy container, route traffic by sub‑domain, secure everything with HTTPS, and simplify deployment and maintenance using Docker‑Compose.
Why You Need an “Nginx Unified Gateway”?
Running each application in its own container quickly leads to a proliferation of exposed ports, scattered IP addresses, and duplicated SSL certificates, which makes firewalls messy, user experience poor, and security weak. A single Nginx proxy that listens only on 80/443 and routes requests by domain solves all these problems.
Port reduction: Only 80 and 443 are open to the outside; internal services stay hidden.
Better UX: Users access services via easy‑to‑remember sub‑domains like www.xlsys.cn instead of IP + port.
Centralised SSL: Nginx handles certificates for all sub‑domains, while backend services communicate over internal HTTP.
Improved security: Backend containers are unreachable from the public network; only Nginx can reach them.
One‑Click Deployment: docker‑compose.yml
version: '3.8'
networks:
secure-net:
driver: bridge
services:
nginx-proxy:
image: nginx:alpine
container_name: nginx-proxy
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/conf.d:/etc/nginx/conf.d
- ./nginx/certs:/etc/nginx/certs
- ./nginx/logs:/var/log/nginx
networks:
- secure-net
restart: unless-stopped
wordpress:
image: wordpress:php8.2-apache
container_name: wordpress
environment:
WORDPRESS_DB_HOST: mysql
WORDPRESS_DB_USER: wpuser
WORDPRESS_DB_PASSWORD: wppass123
WORDPRESS_DB_NAME: wpdb
volumes:
- wordpress_data:/var/www/html
networks:
- secure-net
restart: unless-stopped
depends_on:
- mysql
mysql:
image: mariadb:10.6
container_name: mysql
environment:
MYSQL_ROOT_PASSWORD: rootpass123
MYSQL_DATABASE: wpdb
MYSQL_USER: wpuser
MYSQL_PASSWORD: wppass123
volumes:
- mysql_data:/var/lib/mysql
networks:
- secure-net
restart: unless-stopped
ddns-go:
image: jeessy/ddns-go
container_name: ddns-go
ports:
- "9876:9876"
networks:
- secure-net
volumes:
- ./ddns-go:/root
environment:
- PUID=1000
- PGID=1000
restart: unless-stopped
grafana:
image: grafana/grafana:latest
container_name: grafana
ports:
- "3000"
networks:
- secure-net
volumes:
- grafana_data:/var/lib/grafana
- ./grafana/provisioning:/etc/grafana/provisioning
environment:
- GF_SECURITY_ADMIN_PASSWORD=yourpassword
- GF_USERS_ALLOW_SIGN_UP=false
restart: unless-stopped
uptime-kuma:
image: louislam/uptime-kuma:1
container_name: uptime-kuma
ports:
- "3001"
networks:
- secure-net
volumes:
- uptime_kuma_data:/app/data
restart: unless-stopped
trilium:
image: zadam/trilium:latest
container_name: trilium
ports:
- "8080"
networks:
- secure-net
volumes:
- trilium_data:/home/node/trilium-data
environment:
- TRILIUM_PORT=8080
restart: unless-stopped
volumes:
wordpress_data:
mysql_data:
grafana_data:
uptime_kuma_data:
trilium_data:Nginx Configuration Files (Sub‑domain Routing)
blog.conf (WordPress)
server {
listen 80;
server_name www.xlsys.cn;
location / {
proxy_pass http://wordpress;
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;
}
}ddns.conf (DDNS‑GO)
server {
listen 80;
server_name ***.xlsys.cn;
location / {
proxy_pass http://localhost:9876;
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;
}
}monitor.conf (Uptime Kuma)
server {
listen 80;
server_name ***.xlsys.cn;
location / {
proxy_pass http://uptime-kuma:3001;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_xforwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}metrics.conf (Grafana)
server {
listen 80;
server_name ***.xlsys.cn;
location / {
proxy_pass http://grafana: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;
}
}note.conf (Trilium Notes)
server {
listen 80;
server_name ***.xlsys.cn;
location / {
proxy_pass http://trilium:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_xforwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}Security Recommendations
Enable HTTPS for each sub‑domain using acme.sh or Certbot.
Restrict access to sensitive services (e.g., lab.xlsys.cn) with IP whitelists.
Regularly audit Nginx access logs (e.g., /opt/nginx-proxy/logs/access.log).
Back up persistent volumes such as wordpress_data and trilium_data.
My Actual Results
All services are reachable via *.xlsys.cn, giving a professional look.
Only ports 80/443 are exposed to the internet; internal ports are closed.
Adding a new service only requires dropping a new .conf file, reducing operational overhead.
The setup is ready for WAF, CDN, and full‑site HTTPS.
In summary, an Nginx container can act as a central control panel for all internal services, keeping them hidden, secure, and easy to manage.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Xiao Liu Lab
An operations lab passionate about server tinkering 🔬 Sharing automation scripts, high-availability architecture, alert optimization, and incident reviews. Using technology to reduce overtime and experience to avoid major pitfalls. Follow me for easier, more reliable operations!
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
