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:
Page Contents
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.
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
Create a new ASPNET Core Web App (Razor Pages) in Visual Studio.
Give the app name as MultiApp, and un-check the option Place solution and project in the same directory.
On the next screen do not select Enable Docker option.
Click to create the app.
Add a new project to the same solution. For doing this, right click the solution name in Solution Explorer and select Add >> New Project.
Select ASP.NET Core Web API and click Next button.
Call this new project as MultiApi and click Next button.
In the next screen make sure to un-check the checkbox that says – Configure for HTTPS, and click the Create button.
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.
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()
{
…
}
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.
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>
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.
Now run the app in visual studio, you will see a joke displayed on the page.
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.
Right click the MultiApp project name on the solution explorer and select Add >> Container Orchestrator Support.
The Add Container Orchestrator Support dialog appears. Select Docker Compose and click the OK button.
A new window opens where you need to select Target OS. Here select “Linux” and click “OK” button.
When you click the OK button, Visual Studio creates 2 things:
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:
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.
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.
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"]
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.
The Add Container Orchestrator Support dialog appears, here select Docker Compose and click the OK button.
Next, a new window opens where you need to select Target OS. So, select “Linux” and click “OK” button.
As soon as you click the OK button, Visual Studio will do 2 things:
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.
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.
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.
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.
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.
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.
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.
You can now download the source codes of this tutorial:
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.
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.
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
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
Thanks for providing this solution. This will help other developers facing the same issue.