Multi-Container ASP.NET Core App with Docker Compose

Multi-Container ASP.NET Core App with Docker Compose

In this tutorial I will create a Multi-Container ASP.NET Core App with Docker Compose. Here the containers will communicate with each other. There will be 2 projects in the APP:

  • 1. ASP.NET Core Web API project which will provide a random joke.
  • 2. ASP.NET Core Razor project which will call the Web API project and get a random joke from it. Once the joke is received, it will be shown in an HTML table.

What is Docker Compose

Docker Compose is a tool to run multi-container Docker app. It comes bundled with Docker engine and gets automatically installed when you install docker desktop. You use YAML file to work with docker compose. In the YAML file you define the different containers, images and the apps running on these images. You will also need to define the dockerfiles and other necessary settings.

This tutorial is a part of ASP.NET Core apps on Docker series.

A sample YAML file is defined below:

version: '3.4'

services:
  webfrontend:
    image: ${DOCKER_REGISTRY-}webfrontend
    build:
      context: .
      dockerfile: WebFrontEnd/Dockerfile

  mywebapi:
    image: ${DOCKER_REGISTRY-}mywebapi
    build:
      context: .
      dockerfile: MyWebAPI/Dockerfile

You will understand more about docker compose when we will create the app and run on multiple containers with docker compose.

Creating a Multi-Project ASP.NET Core App

Create a new Web Application in Visual Studio.

asp.net core web application

Give your app the name MultiApp, and un-check the option Place solution and project in the same directory.

configure project

Do not select Enable Docker Support. We will add Docker support later.

I would suggest you to also read ASP.NET Core APP with HTTPS in Docker.

Next, select the template called ASP.NET Core Web App for creating a basic ASP.NET Core Razor Pages based app.

asp.net core razor pages

Create a Web API project

Add a new project to the same solution. For doing this, right click the solution name in Solution Explorer and select Add >> New Project.

adding new project to the solution

Select ASP.NET Core Web Application.

ASP.NET Core Web Application

Call it MultiApi.

new project

Select the template as ASP.NET Core Web API. Make sure to un-check the checkbox that says – Configure for HTTPS.

configure for https

This is done because there is no need for SSL for the communication between Docker Containers, SSL is only needed for communication between host and container. That is when we open the app in the browser by typing it’s URL.

Create API Controller

In the “MultiApi” project create a new class called Joke.cs inside the Models folder with the following code:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace MultiApi.Models
{
    public class Joke
    {
        public string Name { get; set; }
        public string Text { get; set; }
        public string Category { get; set; }
    }
}

This class is for the Jokes that will be randomly send to the client when called.

Next add a new controller called JokeController.cs inside the Controllers folder with the following code:

using Microsoft.AspNetCore.Mvc;
using MultiApi.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace MultiApi.Controllers
{
    [ApiController]
    [Route("api/[controller]")]
    public class JokeController : ControllerBase
    {
        [HttpGet]
        public Joke Get()
        {
            Random rand = new Random();
            int toSkip = rand.Next(0, 10);
            Joke joke = Repository().Skip(toSkip).Take(1).FirstOrDefault();
            return joke;
        }


        public List<Joke> Repository()
        {
            List<Joke> jokeList = new List<Joke> {
                new Joke {Name = "RATTLE SNAKE", Text = "Two men are hiking through the woods when one of them cries out, “Snake! Run!” His companion laughs at him. “Oh, relax. It’s only a baby,” he says. “Don’t you!", Category="Animal" },
                new Joke {Name = "HORSE RIDER", Text = "To be or not to be a horse rider, that is equestrian. —Mark Simmons, comedian", Category="Animal" },
                new Joke {Name = "POSTURE CAT", Text = "What did the grandma cat say to her grandson when she saw him slouching? A: You need to pay more attention to my pawsture.", Category="Animal" },
                new Joke {Name = "HE CAN DO IT HIMSELF", Text = "It was my first night caring for an elderly patient. When he grew sleepy, I wheeled his chair as close to the bed as possible and, using the techniques I’d...", Category="Dockor" },
                new Joke {Name = "ON THE BADGE", Text = "My 85-year-old grandfather was rushed to the hospital with a possible concussion. The doctor asked him a series of questions: “Do you know where you are?” “I’m at Rex Hospital.”...", Category="Dockor" },
                new Joke {Name = "THE NURSE HAS MY TEETH", Text = "As a brain wave technologist, I often ask postoperative patients to smile to make sure their facial nerves are intact. It always struck me as odd to be asking this...", Category="Dockor" },
                new Joke {Name = "GLUTEN ATTACK", Text = "Guy staring at an ambulance in front of Whole Foods: “Somebody must have accidentally eaten gluten.”", Category="Food" },
                new Joke {Name = "MORNING TEA", Text = "What has T in the beginning, T in the middle, and T at the end? A: A teapot.", Category="Food" },
                new Joke {Name = "MAKE ME A SANDWICH", Text = "My husband and I were daydreaming about what we would do if we won the lottery. I started: “I’d hire a cook so that I could just say, ‘Hey, make...", Category="Marriage" },
                new Joke {Name = "SELL IT", Text = "As my wife and I prepared for our garage sale, I came across a painting. Looking at the back, I discovered that I had written “To my beautiful wife on...", Category="Marriage" }
                };

            return jokeList;
        }
    }
}

Notice the 2 attributes on the controller which will make it API Controller and so it will be sending the Joke to the client in json. These attributes are:

[ApiController]
[Route("api/[controller]")]

Also notice that this controller is inherited from ControllerBase.

The Web API controller has a HTTP GET method that will return a random joke.

[HttpGet]
public Joke Get()
{
    Random rand = new Random();
    int toSkip = rand.Next(0, 10);
    Joke joke = Repository().Skip(toSkip).Take(1).FirstOrDefault();
    return joke;
}

There are 10 Jokes contained by the Repository method.

public List<Joke> Repository()
{
…
}
Calling Web API from “MultiApp” project

Now open the Index.cshtml.cs page kept inside “Pages” folder of the MultiApp project and change the OnGet() method code to:

public async Task OnGet()
{
    using (var client = new System.Net.Http.HttpClient())
    {
        var request = new System.Net.Http.HttpRequestMessage();
        request.RequestUri = new Uri("http://localhost:53205/api/Joke");
        var response = await client.SendAsync(request);
        var joke = await response.Content.ReadAsStringAsync();

        var details = JObject.Parse(joke);
        ViewData["Joke"] = details["name"] + ";;" + details["text"] + ";;" + details["category"];
    }
} 

In this method I am calling the Web API kept in the MultiApi project. Notice the url of the web api – http://localhost:53205/api/Joke. You can get this url by selecting properties of the MultiApi project, see below image.

multi api url

The JObject is an object of Newtonsoft.Json.Linq class which is used to convert a string containing json to JObject type. After that you can extract the individual values of the json as:

details["name"]
details["text"]
details["category"]

When the Joke is received, it is saved to a ViewData varible for the index view. So in the Index view I will show the Joke inside an HTML table. So edit the Index.cshtml kept inside the Pages folder to include an HTML Table whose code is shown in highlighted way:

@page
@model IndexModel
@{
    ViewData["Title"] = "Home page";
}

<div class="text-center">
    <h1 class="display-4">Welcome</h1>
    <p>Learn about <a href="https://docs.microsoft.com/aspnet/core">building Web apps with ASP.NET Core</a>.</p>
    <table class="table table-sm table-striped table-bordered m-2">
        <thead>
            <tr>
                <th>Name</th>
                <th>Text</th>
                <th>Category</th>
            </tr>
        </thead>
        <tbody>
            <tr>
                <td>@ViewData["Joke"].ToString().Split(";;")[0]</td>
                <td>@ViewData["Joke"].ToString().Split(";;")[1]</td>
                <td>@ViewData["Joke"].ToString().Split(";;")[2]</td>
            </tr>
        </tbody>
    </table>
</div>
Multiple Startup Projects

Right click the solution name in the solution explorer and select properties. Here select Multiple Startup Project. Make sure to select “Start” for both these project as shown in the below image.

multiple startup projects

Now run the app in visual studio, you will see a joke displayed on the page. Check the below image where I have shown this thing.

joke shown in multiapp project

Creating a Multi-Container from Docker Compose

The Multi Project ASP.NET Core app is ready and it’s time to create Multi-Containers for it using Docker Compose.

The purpose of docker-compose.yml file is to specify the docker engine to run this app in 2 containers.

  • 1. Container to run MultiApp project.
  • 2. Container to run MultiApi project.
multi containers docker compose

Right click the MultiApp project name on the solution explorer and select Add >> Container Orchestrator Support.

Container Orchestrator Support

The Add Container Orchestrator Support dialog appears. Select Docker Compose and click the OK button.

Add Container Orchestrator Support

A new window opens where you need to select Target OS. Here select “Linux” and click “OK” button.

docker support options

As soon as you click the OK button, Visual Studio does 2 things:

1. Create docker-compose project, yaml file, and .dockerignore file

Creates a docker-compose.yml file and a .dockerignore file in a new project called docker-compose. This project is added to the solution. The below image shows this:

docker compose node solution

The docker-compose project is shown in boldface font, as it is also made as startup project.

Also check the Visual Studio menu bar on the top where you can see that this newly created project is set as a startup project for the solution.

docker compose vs menu

The .dockerignore file tells docker server about the file types and extensions that should not be included in the container.

Open the docker-compose.yml, it will have the following code:

version: '3.4'

services:
  multiapp:
    image: ${DOCKER_REGISTRY-}multiapp
    build:
      context: .
      dockerfile: MultiApp/Dockerfile

Let us understand what this yaml file is saying.

Starting from the top it defines a version number which is 3.4 – version: ‘3.4’.

Then there are services which represent the containers that will be created in the application. It specifies a container named multiapp.

services:
  multiapp:

Then the image name is specified. Basically, this image will contain the MultiApp project.

image: ${DOCKER_REGISTRY-}multiapp

Next, there is a “build” section which specify the procedure to build the image. Here we define the path to a directory containing the Dockerfile. It is interpreted as relative to the location of the Compose file.

build:
  context: .
  dockerfile: MultiApp/Dockerfile
2. Create a Dockerfile for the “MultiApp” project

Visual Studio also creates a Dockerfile inside the MultiApp project for the project. It’s code is given below:

#See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging.

FROM mcr.microsoft.com/dotnet/aspnet:5.0-buster-slim AS base
WORKDIR /app
EXPOSE 80
EXPOSE 443

FROM mcr.microsoft.com/dotnet/sdk:5.0-buster-slim AS build
WORKDIR /src
COPY ["MultiApp/MultiApp.csproj", "MultiApp/"]
RUN dotnet restore "MultiApp/MultiApp.csproj"
COPY . .
WORKDIR "/src/MultiApp"
RUN dotnet build "MultiApp.csproj" -c Release -o /app/build

FROM build AS publish
RUN dotnet publish "MultiApp.csproj" -c Release -o /app/publish

FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "MultiApp.dll"]

The Dockerfile creates the Image in layers or steps. I have explained it in full details on my previous tutorial so kindly check it – What is Dockerfile.

Next, you have to do the same thing for adding Container Orchestrator Support for the second project which is the MultiApi.

Right click the MultiApi project name on the solution explorer and select Add >> Container Orchestrator Support.

Container Orchestrator Support

The Add Container Orchestrator Support dialog appears, here select Docker Compose and click the OK button.

Add Container Orchestrator Support

Next, a new window opens where you need to select Target OS. So, select “Linux” and click “OK” button.

docker support options

As soon as you click the OK button, Visual Studio will do 2 things:

  • 1. Create a new Dockerfile for the MultiApi project.
  • 2. Add a new service to the docker-compose.yml file. This service specifies the second container which will run the second project i.e. MultiApi.

Open the docker-compose.yml and you can notice the second service added to it.

version: '3.4'

services:
  multiapp:
    image: ${DOCKER_REGISTRY-}multiapp
    build:
      context: .
      dockerfile: MultiApp/Dockerfile

  multiapi:
    image: ${DOCKER_REGISTRY-}multiapi
    build:
      context: .
      dockerfile: MultiApi/Dockerfile

Congrats, we completed the task to tell docker how to build mulit-containers for our ASP.NET Core App using docker-compose.yml. It’s time to build the image and run the compose.

Docker Compose build and running the app

Before you run the APP in docker, you have to change the url of the Web API. So, open the index.cshtml.cs file located in the Pages folder of MultiApp project. Here change the URL from http://localhost:53205/api/Joke to http://multiapi/api/Joke. See the highlighted code given below:

public async Task OnGet()
{
    using (var client = new System.Net.Http.HttpClient())
    {
        var request = new System.Net.Http.HttpRequestMessage();
        request.RequestUri = new Uri("http://multiapi/api/Joke");
        var response = await client.SendAsync(request);
        var joke = await response.Content.ReadAsStringAsync();

        var details = JObject.Parse(joke);
        ViewData["Joke"] = details["name"] + ";;" + details["text"] + ";;" + details["category"];
    }
}

This is done because now the app will be running into 2 containers therefore you can now use the service name (multiapi) to call the Web API.

You can run the project in visual studio and it will run it from 2 docker containers. I have created a small video which shows the running for this app in vs.

Now check docker desktop which will show the Multi-Containers running for this app.

docker compose docker desktop

Exposing ports and configuring Environment variables for HTTPS

I will now expose the port of the container to the host. This will help to call the container directly with it’s url on the browser (without using visual studio).

So, add the ports and environment section in the docker-compose.yml file.

version: '3.4'

services:
  multiapp:
    image: ${DOCKER_REGISTRY-}multiapp
    build:
      context: .
      dockerfile: MultiApp/Dockerfile
    ports:
      - "9000:443"
      - "9001:80"
    environment:
      - ASPNETCORE_ENVIRONMENT=Development
      - ASPNETCORE_URLS=https://+:443;http://+:80
      - ASPNETCORE_HTTPS_PORT=9000
      - ASPNETCORE_Kestrel__Certificates__Default__Password=mypass123
      - ASPNETCORE_Kestrel__Certificates__Default__Path=/https/aspnetapp.pfx
    volumes:
        - ./MultiApp/https/aspnetapp.pfx:/https/aspnetapp.pfx:ro

  multiapi:
    image: ${DOCKER_REGISTRY-}multiapi
    build:
      context: .
      dockerfile: MultiApi/Dockerfile

With these settings I am specifying 2 ports that will be exposed. Ports are expose in the form of “host port:container port”. So, I exposed port 9000 for https and 9001 for http.

ports: 
  - "9000:443"
  - "9001:80"

Then I specified 5 environment variables to specify environment, urls, https port, ssl certificate password, and ssl path.

environment:
  - ASPNETCORE_ENVIRONMENT=Development
  - ASPNETCORE_URLS=https://+:443;http://+:80
  - ASPNETCORE_HTTPS_PORT=9000
  - ASPNETCORE_Kestrel__Certificates__Default__Password=mypass123
  - ASPNETCORE_Kestrel__Certificates__Default__Path=/https/aspnetapp.pfx

Note that the port 9000 is specified for the https type url of the container running our app. I specified the ssl certificate password as “mypass123”. I will generate this certificate with this password in just a moment.

Also note that with the environment variable ASPNETCORE_Kestrel__Certificates__Default__Path I specified the path in the container where the ssl certificate will be mount. This path is “/https/aspnetapp.pfx”.

Next section is about volume, where I have specified how the certificate will be mount to the container.

volumes:
    - ./MultiApp/https/aspnetapp.pfx:/https/aspnetapp.pfx:ro

Here I am saying that the certificate location on the drive from where it will be mount to the container is “MultiApp/https/aspnetapp.pfx”. The “.” In the beginning tells that the path starts from the directory of the docker-compose.yml file. So “MultiApp” directory is on the same directory where docker-compose.yml file is kept.

The second path defines the path on the container where this ssl certificate will be mount to. This path is “/https/aspnetapp.pfx”.

Generate SSL Certificate for development

I have to generate a new SSL certificate for development using the dotnet dev-certs . So, in your command prompt run the following command:

dotnet dev-certs https -ep %USERPROFILE%\.aspnet\https\aspnetapp.pfx -p mypass123

This will create a development certificate called aspnetapp.pfx with a password mypass123.

The path of the SSL certificate will be inside user profile folder since I have referred it from %USERPROFILE%\. The full path of the SSL in my case is:

C:\Users\Avita\.aspnet\https

Here Avita is my windows login name, change it to your’s login name and you will find it in your pc. In the below image I have shown the SSL certificate file which is just generated on my pc.

generated ssl

So, copy this certificate and paste it to the https folder in the MultiApp project folder. Check the below image where I have marked it:

ssl path

The next command to run is to trust the ASP.NET Core HTTPS development certificate. This command is given below.

dotnet dev-certs https –trust

If you get a prompt after running the above command then make sure you accept it.

Running the App on Docker Compose

The Docker compose has 2 main command for building the services and running the containers. In your command prompt go to the directory of the docker-compose.yml file and run the build command:

docker-compose build

This will Build the services and will create 2 images for our app.

Next, create and start the 2 containers for these 2 images. This command is given below.

docker-compose up

Now you can simply open the url – https://localhost:9000/ to open the multi-container app. I have shown this in the below video.

You can now download the source codes of this tutorial:

Download

Conclusion

In this tutorial I created a multi container ASP.NET Core app using docker compose. These containers also communicate with one another. Later on, I added ssl certificate and exposed the app on https port. Please share this tutorial on facebook, twitter and other websites so that other developers can also use it to build docker multi container app.

SHARE THIS ARTICLE

  • linkedin
  • reddit
yogihosting

ABOUT THE AUTHOR

I hope you enjoyed reading this tutorial. If it helped you then consider buying a cup of coffee for me. This will help me in writing more such good tutorials for the readers. Thank you. Buy Me A Coffee donate

Comments

  1. Kevin says:

    Hi YogYogi,
    Thank you for your great work in Docker tutorial. I have read all of the four Docker articles in your blog; I would like to say they are among the best Docker tutorial for .Net developer.
    All of them have step by step instructions with very clear explanations, related code and screenshots. I believe after reading the articles, everyone will get a good knowledge and will be able to do the tasks specified in the tutorials. A tutorial couldn’t be better than that, for these reasons I believe they are within the best in their topics.
    I only encountered a small issue when replicate the project. When I do the cmd
    dotnet dev-certs https -ep %USERPROFILE%\.aspnet\https\aspnetapp.pfx -p mypass123
    I got a
    Specify –help for a list of available options and commands.
    I am able to run
    dotnet dev-certs –h
    I have googled but didn’t found answers for that although someone else has also encountered same problem in their tasks.
    This is the only issue I got when working with your tutorial, and it is totally an issue on my side.
    Again thank you for the great work you have done. They are very helpful.

    1. yogihosting says:

      Hello Kevin,

      Few things you can do here.
      1. Open command prompt from administrative priviledges. You can do it by right clicking command prompt and select “Run as Administrator”.
      2. Try cleaning the Certificate with dotnet dev-certs https --clean command and then regenerate.

      You can see also my other tutorial on SSL generation – https://www.yogihosting.com/docker-https-aspnet-core see the topic “Creating SSL with dotnet dev-certs”.

      I hope you will be able to solve it.

      Regards,
      Yogi

  2. Kevin says:

    Hi Yogi,
    Thank you for the help.
    I found the CMD in my PC didn’t recognise %USERPROFILE%\.
    I executed it in PowerSell with
    dotnet dev-certs https -ep $env:USERPROFILE\.aspnet\https\aspnetapp.pfx -p mypass123
    It works fine.
    Again thank you very much for the great tutorials you made, including the tutorials of .Net Core. They are the best in their subjects.

    Best regards,
    Kevin

Leave a Reply

Your email address will not be published. Required fields are marked *