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:
Page Contents
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.
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.
Create a new Web Application in Visual Studio.
Give your app the name MultiApp, and un-check the option Place solution and project in the same directory.
Do not select Enable Docker Support. We will add Docker support later.
Next, select the template called ASP.NET Core Web App for creating a basic ASP.NET Core Razor Pages based 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 Application.
Call it MultiApi.
Select the template as ASP.NET Core Web API. Make sure to un-check the checkbox that says – 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.
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()
{
…
}
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.
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>
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. Check the below image where I have shown this thing.
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.
As soon as you click the OK button, Visual Studio does 2 things:
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 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.
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
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.
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 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.
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.
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”.
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.
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:
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.
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:
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.
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