NGINX in Docker as reverse proxy to other containers

It’s quite common to have containers on our VPS. NGINX running inside a container is also a common setup. But how do you properly handle both of these things? I’ll suggest a setup that will isolate each container as much as possible.

The focus here is on the networking side. If we want to expose our Docker containers through NGINX, chances are we don’t want them to be accessed elsewhere. Additionally, when an application comprises multiple sub-services, only one of them may need to be accessible from outside the container (e.g., if an app has its own database container).

info

Note

In this blog post, I will refer to main services on our VPS as “apps.” Examples include NGINX, GitLab, Nextcloud, and WordPress. Each app is composed of one or more sub-services, each in its container (blocks under the services section of the Docker Compose file).

For instance, the WordPress app consists of a frontend sub-service and a database sub-service, each in its own container.

The idea

When using Docker Compose, services within a configuration file can access other services in the same file using their service name as a hostname. In general, containers can access other containers (not necessarily in the same Compose file) using their internal IP address (172.22.*.*). However, this address changes every time the container is restarted. So, how can we ensure consistent communication between two non-related containers?

This is done through Docker networks. The idea is to have a custom network containing NGINX and every container that needs to be accessed from it. We will call this network nginx-net. Furthermore, if an app is composed of many sub-services, it will also have its own private network, to which all sub-services will connect. However, only the sub-service that needs to be accessed from NGINX will also connect to nginx-net.

This ensures that:

  1. The sub-services of an app can still communicate with each other.
  2. NGINX can reach all the desired containers.
  3. None of the networks is accessible from outside the containers.
info

Note

Although containers in a network are completely isolated from containers in another network, they can still reach the internet.

Setting up NGINX

Firstly, we need to create the network that NGINX will use to expose all the necessary services. Execute the following command:

docker network create nginx-net

Here, I use nginx-net as the name of the custom network, but you can choose whatever you want. Once done, we need to set up our NGINX container. Similar to what we did for WordPress, create a directory to store NGINX and its configuration. Create a docker-compose.yml file with the following content:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
version: "3.9"
services:
  nginx:
    container_name: nginx
    image: nginx:latest
    hostname: nginx
    ports:
      - "80:80"
      - "443:443"
    volumes:
      # - ./nginx.conf:/etc/nginx/nginx.conf:ro
      - ./conf:/etc/nginx/conf.d:ro
      - ~/www/:/var/www/sites:ro
    networks:
      - nginx-net
    restart: unless-stopped

networks:
  nginx-net:
    external: true

Notice the highlighted lines. Lines 18-20 define a network to use inside the compose file. The name of the network (lines 15 and 19) must match the name of the network created earlier. The external field tells Compose not to create a new network but to use an existing one.

Of course, you can edit it to suit your needs. For example, you can change the exposed ports (lines 8, 9) or have different configurations for the volumes (lines 11-13). When done, start it by running:

docker compose up -d
lightbulb

Tip

Line 11 is commented because you probably won’t have an nginx.conf in your directory. Once the container is started, execute:

docker exec nginx cat /etc/nginx/nginx.conf > nginx.conf

to save it in the current directory. Then, uncomment line 11 by removing the leading # and restart the container.

Configuration files are placed inside the conf/ directory at the same level as the compose file. A configuration file ends in .conf and looks like this:

1
2
3
4
5
6
7
8
9
server {
  listen  80;

  server_name  blog.example.com;
  
  location / {
    proxy_pass  http://wordpress/;
  }
}
What do those things mean?

See “How nginx processes a request” for more examples on how to configure NGINX.

We will focus on proxy_pass, the main reason for this blog post: proxying requests to a container -- in this example, WordPress.

Every time we add or edit a configuration, run the following command to reload NGINX:

docker exec nginx nginx -s reload

Setting up other containers

Once our NGINX container is up and running, we only need to edit other containers’ configuration files. Let’s take WordPress as an example. Its compose file, in the wordpress/ directory, is as follows:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
version: '3.1'

services:
  wordpress:
    image: wordpress
    restart: always
    environment:
      # ...
    volumes:
      - wordpress:/var/www/html/

  db:
    image: mysql:5.7
    restart: always
    environment:
      # ...
    volumes:
      - db:/var/lib/mysql

volumes:
  db:
  wordpress:

To make the wordpress container connect to nginx, we need to add nginx-net to the list of network of the wordpress service. However, doing this will overwrite the default network behavior for that container, thus it won’t be able to talk to the db service anymore. To prevent this, we must also create another network for the communication between wordpress and db, say wp-net, and connect both to it. Finally, add a hostname to the wordpress service to make it reachable by NGINX using that hostname.

The final configuration is the following (the changed lines are highlighted):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
version: '3.1'

services:
  wordpress:
    image: wordpress
    restart: always
    environment:
      # ...
    volumes:
      - wordpress:/var/www/html/
    networks:
      - nginx-net
      - wp-net
    hostname: wordpress

  db:
    image: mysql:5.7
    restart: always
    environment:
      # ...
    volumes:
      - db:/var/lib/mysql
    networks:
      - wp-net

volumes:
  db:
  wordpress:
  
networks:
  nginx-net:
    external: true
  wp-net:

With this configuration, NGINX will be able to reach the wordpress service by the address http://wordpress.

Advanced configurations: private sub-networks

In the example above, we connect the various containers to the nginx-net network for them to be accessible by NGINX. This, however, exposes all the containers to each other. For instance, if both GitLab and Nextcloud are connected to the nginx-net network, they are also accessible by each other! This could lead to duplicate host names causing conflicts and generating errors.

This can be avoided by creating multiple sub-networks. Instead of creating a single nginx-net network where all containers connect, create multiple custom networks to handle the connection between NGINX and each app. In other words, if we have n services that should interface with NGINX, the latter will be connected to n networks, one for each service. All those networks contain only NGINX and the service itself.

Tags: Docker Docker Hosting Linux Linux NGINX Nginx Setup