Multi-Container ASP.NET Core App with Docker Compose

Multi-Container ASP.NET Core App with Docker Compose

We will create a Multi-Container app in ASP.NET Core Docker Compose. The containers will also communicate with each other. The ASP.NET Core app will contain projects which will be:

  • 1. ASP.NET Core Web API project that will provide a random joke.
  • 2. ASP.NET Core Razor project that 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.

ASP.NET Core Docker Compose

Docker Compose is a tool to run multi-container Docker apps. It comes bundled with Docker engine and gets automatically installed with docker desktop. We use YAML files to work with docker compose. In the YAML file, we define the different containers, images and the apps running on docker images. We also need to define Dockerfiles and other necessary settings of the app.

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 ASP.NET Core Docker Compose when we will create the app and run it on multiple containers with docker compose.

Creating a Multi-Project ASP.NET Core App

Create a new ASPNET Core Web App (Razor Pages) in Visual Studio.

ASPNET Core Web App (Razor Pages)

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

configure project docker compose

On the next screen do not select Enable Docker option.

Docker ASP.NET Core

Click to create the app.

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 API and click Next button.

ASP.NET Core Web API

Call this new project as MultiApi and click Next button.

new web api project in visual studio

In the next screen make sure to un-check the checkbox that says – Configure for HTTPS, and click the Create button.

configure for https

We do this because there is no need for SSL for the communication between Docker Containers. SSL is only needed for communication between host and container, i.e. when we open the app in the browser by typing it’s URL. But don’t worry we will also cover the SSL stuffs in the later half of this tutorial. So be with us.

Create API Controller

In the MultiApi project create a new class called Joke.cs with the below given code. This class is for the Jokes that will be randomly sent to the client when called.

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

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

using Microsoft.AspNetCore.Mvc;

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:5255/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 we call the Web API kept in the MultiApi project. Notice the url of the web api – http://localhost:5255/api/Joke. You can get this url by selecting properties of the MultiApi project. Then go to Debug ➤ Open debug launch profiles UI, see below image.

app url in visual studio

The JObject is an object of Newtonsoft.Json.Linq class which is used to convert a string containing json to JObject type. After that we 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. On the Index view, we will show the Joke inside an HTML table. Therefore 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 visual studio

Now run the app in visual studio, you will see a joke displayed on the page.

ASP.NET Core Multi-Project App

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.

  • First Container runs MultiApp project.
  • Second Container runs MultiApi project.

multi containers asp.net core 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

When you click the OK button, Visual Studio creates 2 things:

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

Visual Studio 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 the 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

Note that another file called docker-compose.override.yml is created to extend services for the docker-compose.yml. You can see it by clicking the arrow given on the front of docker-compose.yml file in visual studio. The docker-compose.override.yml file is shown below:

version: '3.4'

services:
  dockercrud:
    environment:
      - ASPNETCORE_ENVIRONMENT=Development
      - ASPNETCORE_HTTP_PORTS=8080
      - ASPNETCORE_HTTPS_PORTS=8081
    ports:
      - "8080"
      - "8081"
    volumes:
      - ${APPDATA}/Microsoft/UserSecrets:/home/app/.microsoft/usersecrets:ro
      - ${APPDATA}/ASP.NET/Https:/home/app/.aspnet/https:ro

There are environment variables, ports and volumes provided in this file. Docker will ultimately merge all the services written on docker-compose.yml and docker-compose.override.yml when creating the container for the app.

2. Creates 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:8.0 AS base
USER app
WORKDIR /app
EXPOSE 8080
EXPOSE 8081

FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
ARG BUILD_CONFIGURATION=Release
WORKDIR /src
COPY ["MultiApp/MultiApp.csproj", "MultiApp/"]
RUN dotnet restore "./MultiApp/./MultiApp.csproj"
COPY . .
WORKDIR "/src/MultiApp"
RUN dotnet build "./MultiApp.csproj" -c $BUILD_CONFIGURATION -o /app/build

FROM build AS publish
ARG BUILD_CONFIGURATION=Release
RUN dotnet publish "./MultiApp.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false

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. So right click the MultiApi project name on the solution explorer and select Add >> Container Orchestrator Support.

Container Orchestrator Support Visual Studio

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

Add Container Orchestrator Support Visual Studio

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. Creates a new Dockerfile for the MultiApi project.
  • 2. Adds 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 Dockerfile of MultiApi project, you will see EXPOSE 8080. This means it is accessible on port 8080.

FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
USER app
WORKDIR /app
EXPOSE 8080

FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
ARG BUILD_CONFIGURATION=Release
WORKDIR /src
COPY ["MultiApi/MultiApi.csproj", "MultiApi/"]
RUN dotnet restore "./MultiApi/./MultiApi.csproj"
COPY . .
WORKDIR "/src/MultiApi"
RUN dotnet build "./MultiApi.csproj" -c $BUILD_CONFIGURATION -o /app/build

FROM build AS publish
ARG BUILD_CONFIGURATION=Release
RUN dotnet publish "./MultiApi.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false

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

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 multi-containers for our ASP.NET Core App using docker-compose.yml. It’s time to build the images and run the compose.

Docker Compose build and running the app

Before we run the APP in docker, we are required 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:5255/api/Joke to http://multiapi:8080/api/Joke. Here “multiapi” is the service name and 8080 is the port number. 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:8080/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 from 2 Docker Containers therefore we can now use the service name multiapi which is defined on the docker-compose file, and port 8080 which is the port of MultiApi app, to call the Web API.

Run the app in visual studio and it will now run from 2 docker containers. We have created a small video which shows the running for this app in Visual Studio.

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

docker compose docker desktop

ASP.NET Core Docker Expose Ports

We will now expose docker container ports to the host. This will help to call the container directly with it’s URL on the browser. That is we now won’t need to run the app from from Visual Studio.

The docker ports are also needed to be mapped to the ASP.NET Core app running from the containers. For this we use environment variables. So kindly add the ports and environment variables in the docker-compose.yml file as shown below.

version: '3.4'

services:
  multiapp:
    image: ${DOCKER_REGISTRY-}multiapp
    build:
      context: .
      dockerfile: MultiApp/Dockerfile
    ports:
      - "9000:8080"
      - "9001:8081"
    environment:
      - ASPNETCORE_ENVIRONMENT=Development
      - ASPNETCORE_URLS=https://+:8081;http://+:8080
      - ASPNETCORE_HTTPS_PORT=8081
      - 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

Let us explain what we are doing here. We specified 2 docker container ports that will be exposed. Ports are expose in the form of host port:container port. So, we exposed port no 9001 for https and 9000 for http.

ports: 
  - "9000:8080"
  - "9001:8081"

Then we specified 5 environment variables to specify development environment, urls, https port, ssl certificate password, and ssl path. These will be used by the ASP.NET Core docker app.

environment:
  - ASPNETCORE_ENVIRONMENT=Development
  - ASPNETCORE_URLS=https://+:8081;http://+:8080
  - ASPNETCORE_HTTPS_PORT=8081
  - ASPNETCORE_Kestrel__Certificates__Default__Password=mypass123
  - ASPNETCORE_Kestrel__Certificates__Default__Path=/https/aspnetapp.pfx

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

Also note that with the environment variable ASPNETCORE_Kestrel__Certificates__Default__Path, we specified the path in the container where the ssl certificate will be mounted. This path is /https/aspnetapp.pfx.

Next section is about Docker Volume, where we have specified how we are going to mount the SSL Certificate to the container.

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

Here we are saying that the certificate location is on the drive location MultiApp/https/aspnetapp.pfx. From here it will mount to the docker container. The “.” In the beginning tells that the path starts from the directory of the docker-compose.yml file. So this means “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 mount to. This path is /https/aspnetapp.pfx.

Generate SSL Certificate for development

It’s time to generate a new SSL certificate for development using the dotnet dev-certs command. 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 we have referred it from %USERPROFILE%\ therefore the full path of the SSL in our 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 we 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 we have marked this path.

docker ssl path

The next command to run will 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 commands 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 images for the app.

Next, we create and start the 2 containers for these 2 images. The docker compose up command does this work.

docker-compose up

Now you can simply open the url – https://localhost:9001/ on the browser to open the multi-container app. We have shown this in the below image.

docker-compose-build-up-commands

The app runs from 2 docker containers and the SSL Certificate is located externally to the containers. The path of SSL is https folder in the MultiApp project folder. This is a great thing as later on when our SSL expires then we can replace the SSL with a new one on this directory and Docker Volume will automatically mount the new ssl to the container image. We don’t have to re-build images or re-create containers. This concept of Docker Volume is very great for development.

You can now download the source codes of this tutorial:

Download

Conclusion

In this tutorial we created a multi container app in ASP.NET Core Docker compose. These containers also communicate with one another. Later on, we 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 containers 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

  3. yogihosting says:

    Thanks for providing this solution. This will help other developers facing the same issue.

Leave a Reply

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