Skip to content
ohsudo, Home
Networking

Nginx reverse proxy with Docker Compose: in a container or on the host

Put Nginx in front of Docker Compose services: on the host or in a container, connect by service name, fix "host not found in upstream", keep ports private, add HTTPS.

Intermediate3 min readApplies to Nginx 1.18+ · Docker Engine 20.10+ · Docker Compose v2

by OhSudo ·

Part of a learning path:Web servers: nginx and HTTPS in production

There are two good ways to put Nginx in front of your containers, and it’s worth choosing on purpose:

  • Nginx on the host, with containers published on 127.0.0.1. It’s simple, and fits a server that already runs Nginx, or Certbot.
  • Nginx in a container, in the same Compose file. Everything lives in the project, and containers connect by name.

Option 1: Nginx on the host

Publish the app only on localhost, so that it’s not reachable from outside:

services:
app:
image: myapp:1.0
restart: unless-stopped
ports:
- "127.0.0.1:8080:3000"

That’s the important part. Without 127.0.0.1, the port is open to the internet, and Docker’s rules go around ufw. Then Nginx proxies to it, as for any local app:

server {
listen 80;
listen [::]:80;
server_name example.com;
return 301 https://example.com$request_uri;
}
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
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 Nginx Config Generator with these options writes this for your domain. Certificates work as usual, with Certbot on the host.

Option 2: Nginx in a container

services:
nginx:
image: nginx:1.27-alpine
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
- /etc/letsencrypt:/etc/letsencrypt:ro
depends_on:
- app
app:
image: myapp:1.0
restart: unless-stopped
# No ports: only nginx can reach it, by name.

Only nginx publishes ports. Compose puts all services on one network, where each can reach the others by service name, so the app’s port stays private. The Docker Compose Generator can write the rest of the file, with healthchecks and volumes.

Here’s nginx.conf. Note the name app, and that the port is the one inside the container:

server {
listen 80;
server_name example.com;
return 301 https://example.com$request_uri;
}
server {
listen 443 ssl;
server_name example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
location / {
proxy_pass http://app:3000;
proxy_http_version 1.1;
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;
}
}

Apply changes

Terminal window
docker compose up -d
docker compose exec nginx nginx -t
docker compose exec nginx nginx -s reload
docker compose logs -f nginx

A reload picks up a changed nginx.conf, as long as you’ve mounted the file. Some editors replace a file rather than change it, which leaves the container with the old one, so if a reload changes nothing, use docker compose restart nginx.

“host not found in upstream”

nginx: [emerg] host not found in upstream "app" in /etc/nginx/conf.d/default.conf:15

This common error means the name didn’t resolve when Nginx started. The container then exits, and restarts in a loop. Check these:

  • Is the name right? It’s the name under services:, not container_name, nor the image.
  • Are they on the same network? docker network inspect shows who’s on it. Services in different Compose projects need a shared external network.
  • Is app running? If it’s crashing, its name doesn’t resolve. docker compose ps and docker compose logs app tell you.

depends_on only waits for the container to start. With condition: service_healthy and a healthcheck, it waits until the app is ready.

Nginx keeps the old address

Nginx resolves names in proxy_pass once, at startup. If app is recreated, it may get a new IP address, and Nginx keeps calling the old one, giving you a 502. Either reload it after each change, or make it look the name up each time, using Docker’s own DNS:

location / {
resolver 127.0.0.11 valid=30s;
set $upstream http://app:3000;
proxy_pass $upstream;
proxy_set_header Host $host;
}

It also means that Nginx starts even if app is down, and just answers 502 until it’s back.

Certificates in a container

For certificates, Certbot needs port 80, or a shared folder. If Nginx is in a container, the simplest way is keeping Certbot on the host:

Terminal window
sudo certbot certonly --standalone -d example.com \
--pre-hook "docker compose -f /srv/app/compose.yaml stop nginx" \
--post-hook "docker compose -f /srv/app/compose.yaml start nginx"

That’s what the /etc/letsencrypt volume above is for. Mount the whole folder, as live/ holds links into archive/, and a mount of just live/ leaves broken links.

When it goes wrong

You see Means Go to
502 Bad Gateway The app is down, a wrong port, or an old IP Nginx 502
connect() failed (111: Connection refused) from 127.0.0.1 in a container localhost there is Nginx’s own container Connecting to localhost from a container
port is already allocated Something else has 80 or 443 Port already allocated
504 Gateway Time-out The app takes too long Nginx 504
413 on uploads Set client_max_body_size Nginx 413

The Port Lookup tells you what normally runs on a port.

Warning

Don’t publish databases or caches in the same file, as in 5432:5432. Published ports go around ufw, and reach the internet on a server with a public address. Those services need no ports: at all, since your app reaches them by name.

Found this useful? Share it.

Found a mistake? Let us know at contact@ohsudo.com.

Tools

Try it yourself

Articles

Related articles