ASP.NET Core 1.0 is arriving to it’s final development stage, and with the RC2 version Microsoft offers a Go Live License, which means you can deploy .NET Core applications to productive environments with support from his part.

But, how can an ASP.NET Core app be deployed with warranty? In this tutorial, we are going to define and implement a deployment architecture for ASP.NET Core 1.0 using Docker and NGINX. This deployment architecture will support HTTPS and load balancing.

Want to go straight to the solution? Get it from my Github

Target architecture

As I previously said, for this productive deployment, we want to have the following capabilities:

  • Secure data transfer through HTTPS
  • Scalability and high availability
  • Load balancing

Something like this:

target-architecture-docker-nginx-ketrel

Prerequisites

First of all, you need to follow my step-by-step tutorial to create an ASP.NET Core RC2 application as detailed in Running ASP.NET Core 1.0-RC2 in Docker.

Project structure

Using Visual Studio code, start creating the following folder structure:

| / |- doc |- dotnet | |- Controllers | |- View | |- ... | |- Dockerfile |- nginx | |- Dockerfile | |- ... |- docker-compose.yml |- LICENSE |- README.md

Want to go straight to the solution? Get it from my Github

Setting up ASP.NET Core

Add your asp.net core app created from the Running ASP.NET Core 1.0-RC2 in Docker blog post to “dotnet” folder. (Follow this step-by-step tutorial if you haven’t got any ASP.NET Core app).

Then edit /dotnet/Views/Shared/_Layout.cshtml as follows:

© sesispla 2016 - Request processed by: @System.Environment.MachineName

This will print the host who’s processing the request in the page footer. At the end of this step-by-step tutorial it will help us identifying which container has been processed the request.

Setting up NGINX

Create a new folder called “nginx”. and add an nginx.conf file. Put the following content on it:

worker_processes 4; events { worker_connections 1024; } http { # Act as Load Balancer for 4 nodes upstream core-app.local { server nginxkestrel_core-app_1:5000; server nginxkestrel_core-app_2:5000; server nginxkestrel_core-app_3:5000; server nginxkestrel_core-app_4:5000; } # Redirect all HTTP traffic to HTTPS server { listen 80; return 301 https://$host$request_uri; } # HTTPS Server server { listen 443; # Server name. You need a DNS record (or add this hostname to your hosts file) server_name core-app.local; # Digital certificates generated with makecert.sh / makecert.bat ssl_certificate /etc/nginx/server.crt; ssl_certificate_key /etc/nginx/server.key; # SSL configuration ssl on; ssl_session_cache builtin:1000 shared:SSL:10m; ssl_protocols TLSv1 TLSv1.1 TLSv1.2; ssl_ciphers HIGH:!aNULL:!eNULL:!EXPORT:!CAMELLIA:!DES:!MD5:!PSK:!RC4; ssl_prefer_server_ciphers on; # Location configuration to use the core-app.local upstream defined before location / { proxy_pass http://core-app.local; proxy_read_timeout 90; 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; proxy_redirect http://localhost https://core-app.local; } } }

With this we are instructing NGINX to:

  • Act as load balancer
  • Redirect all HTTP requests to HTTPS
  • Create an HTTPS server
  • Use a digital certificate (we are going to generate next)
  • Redirect all incoming traffic to the ASP.NET Core balanced nodes

Generate a DNS record (or add it to your hosts files)

For this deployment scenario we assume our application will run under the DNS record “core-app.local”. So, open your hosts files (C:\Windows\etc\drivers\hosts in Windows or /etc/hosts in Unix) and add the following record:** X.X.X.X core-app.local (please, change X.X.X.X with your Docker machine IP)**

Or, ff you have a DNS server, create a DNS Record for your NGINX server.

The result hosts files in OS X might look more or less like follows

# Host Database # # localhost is used to configure the loopback interface # when the system is booting. Do not change this entry. ## 127.0.0.1 localhost 255.255.255.255 broadcasthost ::1 localhost 10.211.55.17 core-app.local

Important assumption: We assume we have a DNS record (core-app.local in this case). To achieve this, you should add this to your hosts file

Generate the digital certificate

The HTTPS server requires a digital certificate to work. I have added a makecert.sh / makecert.bat to the source code on GitHub (which assumes you have openssl installed on your machine) to easy create it. Get this file, open a Terminal/CMD and execute it:

$ sudo ./makecert.sh Generating a 2048 bit RSA private key ......................+++ ...........+++ writing new private key to 'server.key' ----- You are about to be asked to enter information that will be incorporated into your certificate request. What you are about to enter is what is called a Distinguished Name or a DN. There are quite a few fields but you can leave some blank For some fields there will be a default value, If you enter '.', the field will be left blank. ----- Country Name (2 letter code) [AU]:ES State or Province Name (full name) [Some-State]:Valencia Locality Name (eg, city) []:Valencia Organization Name (eg, company) [Internet Widgits Pty Ltd]:everis Organizational Unit Name (eg, section) []:Technology Common Name (e.g. server FQDN or YOUR name) []:core-app.local Email Address []:sesispla@outlook.com

This command will generate two files: server.crt and server.key. The important part here is “Common Name”, since it should meet the server_name we specified in nginx (“core-app.local” in this case).

You can ask the rest of questions as you want.

NGINX Dockerfile

Time to dockerize our NGINX server. Create/open the “nginx/Dockerfile” file and put the following content:

FROM nginx COPY ./nginx.conf /etc/nginx/nginx.conf COPY ./server.crt /etc/nginx/server.crt COPY ./server.key /etc/nginx/server.key

Easy, isn’t it? Use the nginx image and add the three files we’ve created.

Composing our ASP.NET Core cluster

The final step is to build and run our images containers. Instead of using the docker build / docker run commands, today we are going to use docker-compose.

Compose is a tool for defining and running multi-container Docker applications. With Compose, you use a Compose file to configure your application’s services. Then, using a single command, you create and start all the services from your configuration.To learn more about Compose refer to the Docker Compose documentation

Create a file called docker-compose.yml in the project root and add:

core-app: build: ./dotnet nginx: build: ./nginx container_name: core-app.local ports: - "80:80" - "443:443" links: - core-app

Docker Compose uses yaml notation. With this file, we are instructing Compose to create two Containers (core-app and nginx).

To create core-app, Compose should build the ./dotnet folder

To create nginx, Compose should build the .nginx folder, name it “core-app.local”, listen on both 80 and 443 ports (and forward the traffic to nginx’s container 80 and 443 ports) and link nginx to core-app.

Time to build and run

Now is time to build and run our ASP.NET Core 1.0, production-ready, cluster. Open a terminal/cmd, cd to the project root folder and type:

$ docker-compose build Building core-app Step 1 : FROM microsoft/dotnet ---> f315e6bbb53f Step 2 : RUN printf "deb http://ftp.us.debian.org/debian jessie main\n" >> /etc/apt/sources.list ---> Using cache ---> 655f0a9ef1c3 Step 3 : COPY . /app ---> 3947e06a2d7d Removing intermediate container 8adddf1e1420 Step 4 : WORKDIR /app ---> Running in 6d369e1f881d ---> 3b32959ceb13 Removing intermediate container 6d369e1f881d Step 5 : RUN dotnet restore ---> Running in 527a8d2f1ebf log : Restoring packages for /app/project.json... ... NuGet Config files used: /root/.nuget/NuGet/NuGet.Config Feeds used: https://api.nuget.org/v3/index.json Installed: 210 package(s) to /app/project.json ---> 224bf54d37e8 Removing intermediate container 527a8d2f1ebf Step 6 : RUN dotnet build ---> Running in 9436a21a77cd Project app (.NETCoreApp,Version=v1.0) will be compiled because the version or bitness of the CLI changed since the last build Compiling app for .NETCoreApp,Version=v1.0 Compilation succeeded. 0 Warning(s) 0 Error(s) Time elapsed 00:00:05.4357489 ---> e54d6e85d063 Removing intermediate container 9436a21a77cd Step 7 : EXPOSE 5000/tcp ---> Running in 0537d1f2bb1b ---> 840bd2124fe4 Removing intermediate container 0537d1f2bb1b Step 8 : ENTRYPOINT dotnet run --server.urls http://0.0.0.0:5000 ---> Running in a7e48785b39a ---> 465ba8a6ab9e Removing intermediate container a7e48785b39a Successfully built 465ba8a6ab9e Building nginx Step 1 : FROM nginx ---> 0d409d33b27e Step 2 : COPY ./nginx.conf /etc/nginx/nginx.conf ---> 6d7dee81b665 Removing intermediate container 8617cf8607af Step 3 : COPY ./server.crt /etc/nginx/server.crt ---> 9261eb87e137 Removing intermediate container f4c2de94f4a8 Step 4 : COPY ./server.key /etc/nginx/server.key ---> 5ec782440f48 Removing intermediate container 4addf6e6f971 Successfully built 5ec782440f48

$ docker-compose up Creating nginxkestrel_core-app_1 Creating core-app.local Attaching to nginxkestrel_core-app_1, core-app.local core-app.local | 2016/06/11 10:34:42 [emerg] 1#1: host not found in upstream "nginxkestrel_core-app_2:5000" in /etc/nginx/nginx.conf:8 core-app.local | nginx: [emerg] host not found in upstream "nginxkestrel_core-app_2:5000" in /etc/nginx/nginx.conf:8 core-app.local exited with code 1 core-app_1 | Project app (.NETCoreApp,Version=v1.0) was previously compiled. Skipping compilation. core-app_1 | Hosting environment: Production core-app_1 | Content root path: /app core-app_1 | Now listening on: http://0.0.0.0:5000 core-app_1 | Application started. Press Ctrl+C to shut down.

Ups! It seems that NGINX one of the upstream servers (members of the load balanced cluster) is not up and running. Let’s do an scale instead:

$ docker-compose scale core-app=4 Creating and starting 2 ... done Creating and starting 3 ... done Creating and starting 4 ... done $ docker-compose up nginxkestrel_core-app_3 is up-to-date nginxkestrel_core-app_4 is up-to-date nginxkestrel_core-app_2 is up-to-date nginxkestrel_core-app_1 is up-to-date Starting core-app.local Attaching to nginxkestrel_core-app_3, nginxkestrel_core-app_4, nginxkestrel_core-app_2, nginxkestrel_core-app_1, core-app.local core-app_3 | Project app (.NETCoreApp,Version=v1.0) was previously compiled. Skipping compilation. core-app.local | 2016/06/11 10:38:18 [emerg] 1#1: host not found in upstream "nginxkestrel_core-app_2:5000" in /etc/nginx/nginx.conf:8 core-app.local | nginx: [emerg] host not found in upstream "nginxkestrel_core-app_2:5000" in /etc/nginx/nginx.conf:8 core-app.local exited with code 1 core-app_1 | Project app (.NETCoreApp,Version=v1.0) was previously compiled. Skipping compilation. core-app_1 | Hosting environment: Production core-app_1 | Content root path: /app core-app_1 | Now listening on: http://0.0.0.0:5000 core-app_1 | Application started. Press Ctrl+C to shut down. core-app_2 | Project app (.NETCoreApp,Version=v1.0) was previously compiled. Skipping compilation. core-app_4 | Project app (.NETCoreApp,Version=v1.0) was previously compiled. Skipping compilation. core-app_2 | Hosting environment: Production core-app_2 | Content root path: /app core-app_2 | Now listening on: http://0.0.0.0:5000 core-app_2 | Application started. Press Ctrl+C to shut down. core-app_4 | Hosting environment: Production core-app_4 | Content root path: /app core-app_4 | Now listening on: http://0.0.0.0:5000 core-app_4 | Application started. Press Ctrl+C to shut down. core-app_3 | Hosting environment: Production core-app_3 | Content root path: /app core-app_3 | Now listening on: http://0.0.0.0:5000 core-app_3 | Application started. Press Ctrl+C to shut down.

Soooo much better!  Once scale has been made, the next time you only need to do a “docker-compose up” to relaunch all the 4 core-app nodes and nginx.

Now the application is up and running. Let’s open your favourite browser and type https://core-app.local:

kestrel-nginx-app-1

At the page footer you’ll find the “Request processed by: X” modification we introduced previously. With Docker, 6340ee4af040 is the Container id. Doing a docker ps you’ll know which node has attended the request:

$ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 680ae82601db nginxkestrel_nginx "nginx -g 'daemon off" About an hour ago Up About an hour 0.0.0.0:80->80/tcp, 0.0.0.0:443->443/tcp core-app.local 6340ee4af040 nginxkestrel_core-app "dotnet run --server." About an hour ago Up About an hour 5000/tcp nginxkestrel_core-app_1 0b6633343a49 nginxkestrel_core-app "dotnet run --server." About an hour ago Up About an hour 5000/tcp nginxkestrel_core-app_2 1fb193033287 nginxkestrel_core-app "dotnet run --server." About an hour ago Up About an hour 5000/tcp nginxkestrel_core-app_4 9b645688d6bb nginxkestrel_core-app "dotnet run --server." About an hour ago Up About an hour 5000/tcp nginxkestrel_core-app_3

In this case, the request was processed by node 1 (nginxkestrel_core-app_1). Doing some page refresh will make this “hostname”  change, since NGINX is balancing the load between all the cluster nodes:

kestrel-nginx-request4 kestrel-nginx-request3 kestrel-nginx-request5 kestrel-nginx-request2

Full solution

You can find the full solution in this GitHub repository

Known limitations due to NGINX Free

  1. With NGINX Free, all upstream nodes should be online at nginx start. Otherwise, an startup error will occur and the nginx container will stop.
  2. Upstream nodes are “hardcoded” in the configuration file, and this makes impossible to dynamically scale the cluster.

You can solve both limitations purchasing NGINX Plus, since it offers a dynamic upstream configuration API, amongst other features.

Or trying NGINX Free modules like https://github.com/GUI/nginx-upstream-dynamic-servers