Fun With Docker-Compose Using .NET Core And NGINX

Multi-Container Apps

Docker Compose is a tool (that comes with Docker) that is used to run multi-container Applications. For example, you can have a Web app in a container backed by a database container, which is running in another container or there can be multiple instances of a Web app container with a reversed-proxy container for static contents, load-balancing etc with a backing database.

With docker compose, you can spin up multiple containers with their required configurations, using a single Docker-CLI command. Docker Compose uses a compose YAML file to read the Services (containers in other words) with the configurations and spin up those Services, which are based on YAML compose file. We use "docker-compose" CLI command to up and run with compose.

Here, we will see a very simple example of docker compose with 3 ASP.NET Core Web Applications containers and one NGINX reversed-proxy Server container, which will load-balance HTTP traffic and port-forward among these 3 Web apps, using docker compose. We will be using Docker 1.12 for this demo.

I have three simplest ASP.NET Core Docker images on my local machine. You can use your own Application image too. Each Web app is exposing its Service at the port 5000, 5001 and 5002 respectively. We will build a custom NGINX Docker image, which will hold our NGINX configuration, as we will see next.


Setting Up Nginx Configurations

Let’s configure the NGINX Server first for our case. Create a nginx.conf file in a directory and write the configuration, as shown below.

  1. worker_processes 1;  
  2.   
  3. events {  
  4.     worker_connections 1024;  
  5. }  
  6.   
  7. http {  
  8.     include mime.types;  
  9.      #include the required MIME types  
  10.     for NGINX  
  11.     keepalive_timeout 65;   
  12.  #connection timeout  
  13.     for worker_processes  
  14.   
  15.     upstream dotnetcore {  
  16.         server aspnetcoreapp_A: 5000;  
  17.         server aspnetcoreapp_B: 5001;  
  18.         server aspnetcoreapp_C: 5002;  
  19.     }  
  20.   
  21.     server {  
  22.         listen 80;  
  23.      #port to listen on  
  24.   
  25.         location / {  
  26.             proxy_pass http: //dotnetcore; # Home page for App1  
  27.         }  
  28.     }  
  29. }  
Notice, we are using the name of the containers (that we will define in a bit) instead of a domain or a host name since we want to use the embedded Docker DNS Server, which will look up for the Services in our own user-defined network. Embedded DNS does not work in the same way, as a default bridge in a network. We are using here the NGINX default round-robin load-balancing algorithm for simplicity.

Next, either use Docker volume to mount Nginx configuration or create your own image, which we are going to do here. Create a Dockerfile in the same directory and copy the nginx.conf file, as shown below
  1. FROM nginx  
  2. COPY nginx.conf /etc/nginx/nginx.conf  
Build the image with a name, as shown below.
  1. docker build -t nginx-core   
Now, we have Nginx and ASP.NET Core Docker images ready. Let’s write Docker Compose file.

Docker Compose

As said earlier, Docker Compose uses a YAML file called docker-compose.yaml to configure and spin up the Services. If you are not familiar with YAML syntax, its pretty easy and is an alternative to JSON and XML. Create a Compose file with the name docker-compose.yml with the contents, shown below.
  1. version: '2'  
  2.   
  3. services:  
  4. proxy: # NGINX Service  
  5.   
  6. aspnetcoreapp_A: # Web App1 running at port 5000  
  7.   
  8. aspnetcoreapp_B: # Web App2 running at port 5001  
  9.   
  10. aspnetcoreapp_C: # Web App3 running at port 5002  
  11.   
  12. networks:  
  13. mynetwork: # User-Defined Network with the name mynetwork  
  14. driver: bridge  
First, we define the version number of the Compose, which is version 2 in this case. Now, we are defining four Services here. 3 Web app Services, one Nginx Reversed-Proxy Service with a user defined bridge network with the name mynetwork.

Now, we need to define the Web Application Services with their name and put them into the same network, which we just created i.e. mynetwork, as shown below.
  1. version: '2'  
  2.   
  3. services:  
  4. proxy:  
  5.   
  6. aspnetcoreapp_A:  
  7. image: aspnetcoreapp5000 # ASP.NET Core app running on port 5000  
  8. container_name: "aspnetcoreapp_A" # Named service A  
  9. networks:  
  10. - mynetwork  
  11.   
  12. aspnetcoreapp_B:  
  13. image: aspnetcoreapp5001 # ASP.NET Core app running on port 5001  
  14. container_name: "aspnetcoreapp_B" # Named service B  
  15. networks:  
  16. - mynetwork  
  17.   
  18. aspnetcoreapp_C:  
  19. image: aspnetcoreapp5002 # ASP.NET Core app running on port 5002  
  20. container_name: "aspnetcoreapp_C" # Named service C  
  21. networks:  
  22. - mynetwork  
  23.   
  24. networks:  
  25. mynetwork: # User-Defined Network with the name mynetwork  
  26. driver: bridge  
In the Compose file mentioned above, we configured 3 Web app services. Each has an image name, container name and a network section.

Finally, we define NGINX Service in the Compose file by exposing the port 80 from the container to the host at 8081 (because at 8080 Jenkins is running on my machine) and put it into our network. Hence, the final Compose file will look, as shown below.
  1. version: '2'  
  2.   
  3. services:  
  4. proxy:  
  5. image: nginx-core # Our custom NGINX Docker image  
  6. container_name: "nginxcore" # Name the service  
  7. ports:   
  8. "8081:80" # Expose the port 80 from the service to the host at 8081  
  9. networks:  
  10. - mynetwork  
  11.   
  12. aspnetcoreapp_A:  
  13. image: aspnetcoreapp5000 # ASP.NET Core app running on port 5000  
  14. container_name: "aspnetcoreapp_A" # Named service A  
  15. networks:  
  16. - mynetwork  
  17.   
  18. aspnetcoreapp_B:  
  19. image: aspnetcoreapp5001 # ASP.NET Core app running on port 5001  
  20. container_name: "aspnetcoreapp_B" # Named service B  
  21. networks:  
  22. - mynetwork  
  23.   
  24. aspnetcoreapp_C:  
  25. image: aspnetcoreapp5002 # ASP.NET Core app running on port 5002  
  26. container_name: "aspnetcoreapp_C" # Named service C  
  27. networks:  
  28. - mynetwork  
  29.   
  30. networks:  
  31. mynetwork: # User-Defined Network with the name mynetwork  
  32. driver: bridge  
Remember, you must have to put the Services into a separate user-defined network because service discovery does not work on the default network. Also, do not expose the ports of your Web Applications directly to the host. Instead, expose the reversed-proxy Server (NGINX) ports to the host.

The name of the containers and ports must match with the Server names, which are defined in NGINX configuration. Now, open up the terminal and type.
  1. docker-compose up -d  
This will create a user-defined network "mynetwork", spin-up the Web apps and NGINX container.

Navigate to the localhost:8081 in the Browser and you will see our Applications up and running.



If I refresh the page, we can see that NGINX is forwarding the port at the different Web app containers, which are running at the different ports due to NGINX HTTP load balancing feature.



Conclusion

The example which I showed here can be very useful in production use-case. I did everything here on one Docker host to help the users to understand the basics. We can use this NGINX load-balancing in a Swarm Cluster (Although load balancing is now supported by default in Docker 1.12 Swarm mode). Also, NGINX can be used in a Swarm cluster for SSL terminations, routing based on the contents, authorizations, rewrites and redirections.