NGINX Reverse Proxy and Load Balancing for ASP.NET 5 Applications with Docker Compose

In this post, I want to show you how it would look like to expose ASP.NET 5 through NGINX, provide a simple load balancing mechanism running locally and orchestrate this through Docker Compose.
17 January 2016
6 minutes read

Related Posts

We have a lot of hosting options with ASP.NET 5 under different operating systems and under different web servers like IIS. Filip W has a great blog post on Running ASP.NET 5 website under IIS. Here, I want to show you how it would look like to expose ASP.NET 5 through NGINX, provide a simple load balancing mechanism running locally and orchestrate this through Docker Compose.

It is not like we didn't have these options before in .NET web development world. To give you an example, you can perfectly run an ASP.NET Web API application under mono and expose it to outside world behind NGINX. However, ASP.NET 5 makes these options really straight forward to adopt.

The end result we will achieve here will have the below look and you can see the sample I have put together for this here:

arch-diagram

ASP.NET 5 Application on RC1

For this sample, I have a very simple APS.NET 5 application which gives you an hello message and lists the environment variables available under the machine. The project structure looks like this:

tugberk@ubuntu:~/apps/aspnet-5-samples/nginx-lb-sample$ tree
.
├── docker-compose.yml
├── docker-nginx.dockerfile
├── docker-webapp.dockerfile
├── global.json
├── nginx.conf
├── NuGet.Config
├── README.md
└── WebApp
    ├── hosting.json
    ├── project.json
    └── Startup.cs

I am not going to put the application code here but you can find the entire code here. However, there is one important thing that I want to mention which is the server URL you will expose ASP.NET 5 application through Kestrel. To make Docker happy, we need to expose the application through "0.0.0.0" rather than localhost or 127.0.0.1. Mark Rendle has a great resource on this explaining why and I have the following hosting.json file which also covers this issue:

{
    "server": "Microsoft.AspNet.Server.Kestrel",
    "server.urls": "http://0.0.0.0:5090"
}

Running ASP.NET 5 Application under Docker

The next step is to run the ASP.NET 5 application under Docker. With the ASP.NET Docker image on Docker Hub, this is insanely simple. Again, Mark Rendle has three amazing posts on ASP.NET 5, Docker and Linux combination as Part 1, Part 2 and Part 3. I strongly encourage you to check them out. For my sample here, I have the below Dockerfile (reference to the file):

FROM microsoft/aspnet:1.0.0-rc1-update1

COPY ./WebApp/project.json /app/WebApp/
COPY ./NuGet.Config /app/
COPY ./global.json /app/
WORKDIR /app/WebApp
RUN ["dnu", "restore"]
ADD ./WebApp /app/WebApp/

EXPOSE 5090
ENTRYPOINT ["dnx", "run"]

That's all I need to be able to run my ASP.NET 5 application under Docker. What I can do now is to build the Docker image and run it:

docker build -f docker-webapp.dockerfile -t hellowebapp .
docker run -d -p 5090:5090 hellowebapp

The container now running in a detached mode and you should be able to hit the HTTP endpoint from your host:

image

From there, you do whatever you want to the container. Rebuild it, stop it, remove it, so and so forth.

NGINX and Docker Compose

Last pieces of the puzzle here are NGINX and Docker Compose. For those of who don't know what NGINX is: NGINX is a free, open-source, high-performance HTTP server and reverse proxy. Under production, you really don't want to expose Kestrel to outside world directly. Instead, you should put Kestrel behind a mature web server like NGINX, IIS or Apache Web Server.

There are two great videos you can watch on Kestrel and Linux hosting which gives you the reasons why you should put Kestrel behind a web server. I strongly encourage you to check them out before putting your application on production in Linux.

Docker Compose, on the other hand, is a completely different type of tool. It is a tool for defining and running multi-container Docker applications. With Compose, you use a Compose file (which is a YAML file) to configure your application’s services. This is a perfect fit for what we want to achieve here since we will have at least three containers running:

  • ASP.NET 5 application 1 Container: An instance of the ASP.NET 5 application
  • ASP.NET 5 application 2 Container: Another instance of the ASP.NET 5 application
  • NGINX Container: An NGINX process which will proxy the requests to ASP.NET 5 applications.

Let's start with configuring NGINX first and make it possible to run under Docker. This is going to very easy as NGINX also has an image up on Docker Hub. We will use this image and tell NGINX to read our config file which looks like this:

worker_processes 4;

events { worker_connections 1024; }

http {
    upstream web-app {
        server webapp1:5090;
        server webapp2:5090;
    }

    server {
      listen 80;

      location / {
        proxy_pass http://web-app;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection keep-alive;
        proxy_set_header Host $host;
        proxy_cache_bypass $http_upgrade;
      }
    }
}

This configuration file has some generic stuff in it but the most importantly, it has our load balancing and reverse proxy configuration. This configuration tells NGINX to accept requests on port 80 and proxy those requests to webapp1:5090 and webapp2:5090. Check out the NGINX reverse proxy guide and load balancing guide for more information about how you can customize the way you are doing the proxying and load balancing but the above configuration is enough for this sample.

There is also an important part in this NGINX configuration to make Kestrel happy. Kestrel has an annoying bug in RC1 which has been already fixed for RC2. To work around the issue, you need to set "Connection: keep-alive" header which is what we are doing with "proxy_set_header Connection keep-alive;" declaration in our NGINX configuration.

Here is what NGINX Dockerfile looks like (reference to the file):

FROM nginx
COPY ./nginx.conf /etc/nginx/nginx.conf

You might wonder at this point about what webapp1 and webapp2 (which we have indicated inside the NGINX configuration file) map to. These are the DNS references for the containers which will run our ASP.NET 5 applications and when we link them in our Docker Compose file, the DNS mapping will happen automatically for container names. Finally, here is what out composition looks like inside the Docker Compose file (reference to the file):

webapp1:
  build: .
  dockerfile: docker-webapp.dockerfile
  container_name: hasample_webapp1
  ports:
    - "5091:5090"
    
webapp2:
  build: .
  dockerfile: docker-webapp.dockerfile
  container_name: hasample_webapp2
  ports:
    - "5092:5090"

nginx:
  build: .
  dockerfile: docker-nginx.dockerfile
  container_name: hasample_nginx
  ports:
    - "5000:80"
  links:
    - webapp1
    - webapp2

You can see under the third container definition, we linked previously defined two containers to NGINX container. Alternatively, you may want to look at Service Discovery in context of Docker instead of linking.

Now we have everything in place and all we need to do is to run two docker-compose commands (under the directory where we have Docker Compose file) to get the application up and running:

docker-compose build
docker-compose up

After these, we should see three containers running. We should also be able to hit localhost:5000 from the host machine and see that the load is being distributed to both ASP.NET 5 application containers:

compose2

Pretty great! However, this is just sample for demo purposes to show how simple it is to have an environment like this up an running locally. This probably provides no performance gains when you run all containers in one box. My next step is going to be to get HAProxy in this mix and let it do the load balancing instead.