Your website is slow. Or worse, it's down. And when you open the log, you can't tell if the problem is the application, the database, or the web server. If you're running multiple services on a single machine, you know the bottleneck is almost always there: an Nginx installed by hand, with configurations layered over time, that no one dares to touch. We, at Meteora Web, see it every day in the projects that come to us. The solution isn't another config file: it's changing your approach. Put Nginx in a Docker container, use it as the single gateway for all your services. It's not a trend. It's a way to make your stack reproducible, testable, and — most importantly — non-destructive.
Why use Nginx with Docker as a gateway for your services?
Think of the gateway as your company's switchboard. Every call enters through one number and gets routed to the right department. Without a switchboard, each department would have its own number, and clients would never know who to call. In your stack, Nginx is that switchboard: it receives all HTTP requests and forwards them to the right service — a Node container, a Python API, a static file. Putting it in Docker means the switchboard is no longer a physical device that breaks: it's software you can recreate identically in five minutes, on any machine.
The concrete benefits? Isolation: if the gateway crashes, it doesn't touch other containers. Reproducibility: the configuration is code, versionable in Git. Scalability: you can duplicate the gateway or the services behind it without touching the operating system. And most importantly: no more "it works on my machine". If it works in the container, it works everywhere.
Common mistake to avoid: don't install Nginx directly on the host and then "run it alongside" the containers. This creates port conflicts, duplicate configurations, and a maintenance nightmare. The gateway must be a container like the others, orchestrated by the same compose file.
How to structure docker-compose for Nginx as a gateway
The docker-compose.yml file is your map. Define services, networks, and volumes. Here's a working base you can copy and adapt:
Sponsored Protocol
version: '3.8'
services:
nginx:
image: nginx:1.27-alpine
container_name: gateway
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/conf.d:/etc/nginx/conf.d:ro
- ./nginx/ssl:/etc/nginx/ssl:ro
- ./www:/var/www/html:ro
networks:
- frontend
restart: unless-stopped
app1:
image: node:20-alpine
# ... your app config
networks:
- frontend
- backend
db:
image: postgres:16
# ... database config
networks:
- backend
networks:
frontend:
driver: bridge
backend:
driver: bridge
Note the two networks: frontend and backend. The gateway is only on the frontend network, the database only on the backend, and the app on both. This is the safest pattern: the database is not reachable from outside, the gateway cannot talk directly to the DB. If an attacker compromises Nginx, they don't get access to your data.
How to configure the nginx.conf file for dynamic routing?
The heart of the gateway is the configuration. Forget huge, monolithic files. In Docker, each site or service has its own file inside conf.d. Here's an example for forwarding requests to a containerized application:
# /etc/nginx/conf.d/app1.conf
server {
listen 80;
server_name app1.example.com;
location / {
proxy_pass http://app1: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;
}
}
The name app1 in proxy_pass is the service name in the compose file. Docker automatically resolves internal DNS. This is the huge advantage: you don't need to know container IPs, they change every time you recreate them. Use service names.
Common mistake: forgetting the X-Forwarded-* headers. Without them, your app behind the proxy doesn't see the visitor's real IP, but the Nginx container's IP. If you do geolocation, rate limiting, or security logging, you lose the most important data.
Sponsored Protocol
How to handle multiple services with Nginx in Docker
You have three services: a WordPress blog, a Laravel API, and a static frontend. With the gateway, each has its own server block. Here's an example of multi-service configuration:
# blog.conf
server {
listen 80;
server_name blog.example.com;
location / {
proxy_pass http://wordpress:80;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
# api.conf
server {
listen 80;
server_name api.example.com;
location / {
proxy_pass http://laravel:9000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
# static.conf
server {
listen 80;
server_name static.example.com;
root /var/www/html;
index index.html;
}
Each file is independent. Modify one without touching the others. And the best part? You can test the configuration before applying it. In the next section, we'll see how.
How to test and reload the configuration without downtime?
The scariest moment: changing the gateway configuration and fearing you'll take the site down. With Docker, the risk drops to almost zero. Here's the procedure we use in production:
- Edit the configuration file on your host.
- Test the syntax with:
docker exec gateway nginx -t - If the test passes, reload with:
docker exec gateway nginx -s reload
The reload command is the secret: Nginx reloads the configuration without interrupting active connections. Current clients don't even see a millisecond of pause. And if the test fails, Nginx continues with the old configuration. No downtime, no panic.
Common mistake: recreating the container on every change with docker-compose up -d. This forces a restart and can cause a brief interruption. Always use reload for Nginx configuration changes, not restart.
How to automate reload with a bind mount and a watchdog
Want maximum automation? Mount the configuration directory as a volume and use a script that watches for changes. Here's an example with inotifywait:
Sponsored Protocol
#!/bin/bash
while inotifywait -r -e modify,create,delete /path/to/nginx/conf.d; do
docker exec gateway nginx -t && docker exec gateway nginx -s reload
done
Every time you save a file, the gateway reloads itself. But be careful: this script is useful in development, not in production. In production, use a CI/CD pipeline that tests the configuration before applying it.
How to manage SSL and HTTPS in the Nginx container?
Security is not optional. If your gateway only speaks HTTP, you're sending client data in plain text. With Docker, certificate management is clean: you mount SSL files as a volume and reference them in the configuration. Here's an example with Let's Encrypt certificates:
server {
listen 443 ssl;
server_name secure.example.com;
ssl_certificate /etc/nginx/ssl/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
location / {
proxy_pass http://app1:3000;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
The ./nginx/ssl volume in the compose file must contain the certificates. You can generate certificates with certbot on the host and then mount them, or use a dedicated container like certbot that renews them automatically. We prefer the latter: renewal is a problem you don't want to handle manually.
Common mistake: forgetting the HTTP to HTTPS redirect. Add a second server block that redirects all traffic:
server {
listen 80;
server_name secure.example.com;
return 301 https://$host$request_uri;
}
This ensures no visitor accidentally lands on the insecure version.
How to monitor Nginx container logs in real time?
A gateway without logs is like an accountant without records: you're flying blind. Nginx logs tell you who accesses, what they request, and what fails. In Docker, logs go to stdout and stderr — and Docker captures them automatically. To view them in real time:
Sponsored Protocol
# Follow the container logs
docker logs -f gateway
# See only the last 100 lines
docker logs --tail 100 gateway
But terminal logs aren't enough for analysis. We recommend mounting logs to a volume and using them with an aggregation tool. Here's how to modify the compose to export logs:
services:
nginx:
volumes:
- ./logs:/var/log/nginx
This way, access.log and error.log are on the host, ready for analysis or sending to a system like ELK or Grafana. The log is your historical memory: if something goes wrong one day, that's where you'll find the answer.
How to optimize Nginx gateway performance in Docker?
A slow gateway is a bottleneck for the whole system. Here are the adjustments we make, based on years of experience with clients who have traffic spikes:
- Keepalive: enable persistent connections to backend services. In the
upstreamblock:
upstream app_backend {
server app1:3000;
keepalive 32;
}
server {
location / {
proxy_pass http://app_backend;
proxy_http_version 1.1;
proxy_set_header Connection "";
}
}
This reduces the number of TCP handshakes and improves latency. Always test: use tools like ab or wrk to measure requests per second before and after the change.
- Compression: enable gzip to reduce traffic. But be careful: don't compress images or videos, you waste CPU. Only text, JSON, HTML.
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml;
gzip_min_length 1024;
- Connection limits: protect the gateway from DoS attacks with
limit_req:
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;
server {
location /api/ {
limit_req zone=api_limit burst=20 nodelay;
proxy_pass http://app_backend;
}
}
This limits to 10 requests per second per IP, with a burst of 20. If a rogue bot hammers you, it won't take down the server.
Sponsored Protocol
What mistakes to avoid when using Nginx with Docker in production?
We've seen too many projects ruined by trivial mistakes. Here are the three most common:
1. Mounting configuration files without read-only mode. If the container can write to the configuration, an attacker who compromises Nginx can modify routing rules and hijack traffic. Always use :ro in the volume.
2. Using latest as the image tag. nginx:latest changes with every release, and what works today might break tomorrow. Pin the tag to a specific version, like nginx:1.27-alpine. We always use Alpine versions: lighter, more secure, less attack surface.
3. Not setting restart: unless-stopped. If the container dies due to an error, Docker restarts it automatically. Without this directive, the gateway stays down until you intervene. And in production, every minute of downtime is lost money.
What to do now
You don't need to rewrite your entire stack in one night. But you can start moving right away with these concrete steps:
- Take an existing service — even just a static site — and put Nginx in Docker as a gateway. Copy the example compose file and adapt it.
- Test reload without fear — modify a config file, run
docker exec gateway nginx -tand thenreload. Verify the site doesn't go down. - Add HTTPS — generate a Let's Encrypt certificate and configure it in the container. Every day without HTTPS is a day your clients' data is exposed.
- Set connection limits — even if you don't have an attack right now, protection is like insurance: you want it before the incident.
- Read the official documentation — the reference guide for Nginx is nginx.org and for Docker Compose docs.docker.com. We consult them daily, and you should too.
If you want to dive deeper into full Nginx management in production, start with our pillar guide on Nginx. And if you need a hand with your stack, we at Meteora Web work on these technologies every day. Your gateway doesn't have to be a point of fragility: it can become your strength.