Search icon CANCEL
Subscription
0
Cart icon
Your Cart (0 item)
Close icon
You have no products in your basket yet
Save more on your purchases! discount-offer-chevron-icon
Savings automatically calculated. No voucher code required.
Arrow left icon
Explore Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Newsletter Hub
Free Learning
Arrow right icon
timer SALE ENDS IN
0 Days
:
00 Hours
:
00 Minutes
:
00 Seconds
Arrow up icon
GO TO TOP
Pragmatic Microservices with C# and Azure

You're reading from   Pragmatic Microservices with C# and Azure Build, deploy, and scale microservices efficiently to meet modern software demands

Arrow left icon
Product type Paperback
Published in May 2024
Publisher Packt
ISBN-13 9781835088296
Length 508 pages
Edition 1st Edition
Languages
Tools
Arrow right icon
Author (1):
Arrow left icon
Christian Nagel Christian Nagel
Author Profile Icon Christian Nagel
Christian Nagel
Arrow right icon
View More author details
Toc

Table of Contents (23) Chapters Close

Preface 1. Part 1: Creating Microservices with .NET
2. Chapter 1: Introduction to .NET Aspire and Microservices FREE CHAPTER 3. Chapter 2: Minimal APIs – Creating REST Services 4. Chapter 3: Writing Data to Relational and NoSQL Databases 5. Chapter 4: Creating Libraries for Client Applications 6. Part 2: Hosting and Deploying
7. Chapter 5: Containerization of Microservices 8. Chapter 6: Microsoft Azure for Hosting Applications 9. Chapter 7: Flexible Configurations 10. Chapter 8: CI/CD – Publishing with GitHub Actions 11. Chapter 9: Authentication and Authorization with Services and Clients 12. Part 3: Troubleshooting and Scaling
13. Chapter 10: All About Testing the Solution 14. Chapter 11: Logging and Monitoring 15. Chapter 12: Scaling Services 16. Part 4: More communication options
17. Chapter 13: Real-Time Messaging with SignalR 18. Chapter 14: gRPC for Binary Communication 19. Chapter 15: Asynchronous Communication with Messages and Events 20. Chapter 16: Running Applications On-Premises and in the Cloud 21. Index 22. Other Books You May Enjoy

Building a Docker image

The .NET CLI dotnet publish command supports creating Docker images without using a Dockerfile. However, to understand Docker, we need to know about Dockerfiles. That’s why we start building a Docker image defining a Dockerfile first.

In this section, we will do the following:

  • Create a Dockerfile for the games API
  • Build a Docker image using the Dockerfile
  • Run the games API with a Docker container
  • Create a Docker image using dotnet publish

Creating a Dockerfile

Docker images are created using instructions in Dockerfiles. Using Visual Studio, you can easily create a Dockerfile from Solution Explorer, using Add | Docker Support. Make sure to select Dockerfile for the Container build type option. Adding a Dockerfile to the Codebreaker.GamesAPI project creates a multi-stage Dockerfile. A multi-stage Dockerfile creates interim images for different stages.

Base stage

With the following code snippets, the different stages are explained. The first stage prepares a Docker image for the production environment:

Codebreaker.GameAPIs/Dockerfile

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

Every Dockerfile starts with a FROM instruction. The FROM instruction defines the base image that is used. mcr.microsoft.com/dotnet/aspnet is an image optimized for production. With .NET 8, this image is based on Debian 12 (Bookworm). The Debian:12-slim image is defined by a Dockerfile with FROM scratch, so this is the root of the hierarchy of instructions. The dotnet/aspnet image contains the .NET runtime and the ASP.NET Core runtime. The .NET SDK is not available with this image. The AS instruction defines a name that allows using the result of this stage with another stage. The USER instruction defines the user that should be used with the instructions of the stage. The WORKDIR instruction sets the working directory for following instructions. If the directory does not exist in the image, it’s created. The last step in the first stage is the EXPOSE instruction. With EXPOSE, you define the ports that the application is listening to. By default, TCP is used, but you can also specify to have an UDP receiver.

Note

.NET 8 has some changes with Docker image generation: the default container images run with non-root users (the app user), and the default port is no longer port 80. Port 80 is a privileged port that requires the root user. The new default port is now 8080.

Build stage

The second stage builds the ASP.NET Core application:

Codebreaker.GameAPIs/Dockerfile

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

With the second stage, we ignore the first stage for a moment and use a different base image: dotnet/sdk. This image contains the .NET SDK and is used to build the application. First, a src directory is created, and the current directory is set to src. The ARG instruction specifies an argument that can be passed when invoking building the Docker image. If this argument is not passed, the default value is Release. Next, you’ll see multiple COPY instructions to copy the project files to subfolders within the current directory. The project files contain the package references. In case the dotnet restore command that is started using the RUN instruction fails, there’s no need to continue with the next steps. dotnet restore downloads the NuGet packages. In case you use a different NuGet feed, the Dockerfile needs some changes to copy nuget.config as well. When dotnet restore succeeds, the complete source code from . is copied to the current directory (which is src at this time) with the COPY instruction. Next, the working directory is changed to the directory of the games API project, and the dotnet build command is invoked to create release code for the application. With dotnet build, the BUILD_CONFIGURATION argument is used, which was specified with ARG. Having the release build in the src/app/build folder is the result for the interim image after the last command.

Note

To make sure that unnecessary files are not copied with instructions such as COPY . ., the .dockerignore file is used. Similar to the .gitignore file where files are specified to be ignored, with a .dockerignore file, you specify what files should not be copied to the image.

Publish stage

Next, the second stage is used as a base for the third stage:

Codebreaker.GameAPIs/Dockerfile

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

The FROM build instruction uses the result from the previous stage and continues here. The dotnet publish command results in code that’s needed to publish the application. The files needed for publication are copied to the /src/app/publish folder. While the working directory was configured with the previous stage, continuing on the build image, the working directory is still set.

Final stage

With the final stage, we continue with the first stage, which was named base:

Codebreaker.GameAPIs/Dockerfile

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

The first instruction with this stage is to set the working directory to app. Then, referencing the third state with --from=publish, the /app/publish directory from the publish stage is copied to the current directory. The ENTRYPOINT instruction defines what should be done on running the image: the dotnet bootstrapper command starts and receives Codebreaker.GameAPIs.dll as an argument. You can do this from the command line as well: dotnet Codebreaker.GameAPIs.dll starts the entry point of the application to kick off the Kestrel server, and the application is available to receive requests.

Before building the Dockerfile, make sure the DataStore configuration in appsettings.json is set to InMemory to use the in-memory provider by default when starting the container.

Building a Docker image with a Dockerfile

To build the image, set the current directory to the directory of the solution, and use this command:

docker build . -f Codebreaker.GameAPIs\Dockerfile -t codebreaker/gamesapi:3.5.3 -t codebreaker/gamesapi:latest

With simple Dockerfiles, using just docker build can be enough to build the image. However, the Dockerfile we use contains references to other projects. With this, we need to pay attention to the context. With the games API service, multiple projects need to be compiled, and the paths used as specified with the Dockerfile use the parent directory. Setting the current directory to the directory of the solution, the context is set to this directory with the first argument after the build command (.). The -f option next references the location of the Dockerfile. With the -t option, the image is tagged. The repository name (codebreaker/gamesapi) needs to be lowercase, followed by the 3.5.3 tag name and the latest tag name. Tag names can be strings; there’s no requirement on the version. It’s just a good practice to always tag the latest version with the latest tag. Specifying the -t option two times, we get two image names that reference the same image with the same image identifier.

To list the images built, use docker images. To restrict the output to codebreaker images, you can define a filter:

docker images codebreaker/*

To check a Docker image for how it was built, we can use the docker history command:

docker history codebreaker/gamesapi:latest

Figure 5.3 shows the result of the docker history command. This shows every instruction from the Dockerfile that was used to build the image. Of course, what you don’t see with the codebreaker/gamesapi image is the dotnet build and dotnet restore commands. The build and publish stages have only been used for interim images to build the application and to create the files needed. Comparing the output to the Dockerfile we created, you see ENTRYPOINT on top, followed by COPY, WORKDIR, and so on. With each of these instructions, you can also see the size result of the instruction. The COPY command copied 3,441 MB into the image. The USER instruction was the first instruction coming from our Dockerfile; the instructions before that (in the lines below the USER instruction) show the instructions when the base image was created:

Figure 5.3 – Result of docker history command

Figure 5.3 – Result of docker history command

To see the exposed ports, environmental variables, the entry point, and more of an image, use the following command:

docker image inspect codebreaker/gamesapi:latest

The result is presented in JSON information and shows information such as exposed ports, environment variables, the entry point, the operating system, and the architecture this image is based on. When running the image, it helps to know what port number needs to be mapped and which environment variables could be useful to override.

Running the games API using Docker

You can start the Docker image of the games API service with the following command:

docker run -p 8080:8080 -d codebreaker/gamesapi:latest

Then, you can check the log output using docker logs <container-id> (get the ID of the running container with docker ps). You can interact with the games service using any client using the following HTTP address: http://localhost:8080.

With the default configuration of the data store, games are just stored in memory. To change this, we need to do the following:

  1. Configure a network to let multiple Docker containers directly communicate
  2. Start the Docker container for the SQL Server instance
  3. Pass configuration values to the Docker container for the games API to use the SQL Server instance

Configuring a network for Docker containers

To let containers communicate with each other, we create a network:

docker network create codebreakernet

Docker supports multiple network types, which can be set using the --driver option. The default is a bridge network where containers within the same bridge network can directly communicate with each other. Use docker network ls to show all networks.

Starting the Docker container with SQL Server

To start the Docker container for SQL Server where the database already exists, you can either use the Docker image we committed or access the state of the previous running container. Here, we do the latter, where we define the name sql1 as the name of the container:

docker start sql1

Use docker ps to check for the running container and see the port mapping as it was defined earlier.

Using the command adds the running container to the codebreakernet network:

docker network connect codebreakernet sql1

Starting the Docker container with the games API

Now, we need to start the games API but override the configuration values. This can be done by setting environmental variables. Passing environmental variables at the start of a Docker container can not only be done by setting the -e option but also by using --env-file and passing a file with environmental variables. This is the content of the gameapis.env file:

gameapis.env

DataStore=SqlServer
ConnectionStrings__GamesSqlServerConnection=sql1,1433;database=CodebreakerGames;user id=sa;password=<enter your password>;TrustServerCertificate=true

Creating the DI container of the application with the default container registers multiple configuration providers. A provider that uses environmental variables wins against the JSON file providers because of the order the providers are configured with the WebApplicationBuilder class (or the Host class). The DataStore key is used to select the storage provider. GamesSqlServerConnection is a key within the hierarchy of ConnectionStrings. Using command-line arguments to pass configuration values, you can specify a hierarchy of configuration values using : as a separator; for example, pass ConnectionStrings:GamesSqlServerConnection. Using : does not work everywhere; for example, with environmental variables on Linux. Using __ translates to this hierarchy.

With the environment variables file, the connection to the SQL Server Docker container is using the hostname of the Docker container and the port that is used by SQL Server. Being in the same network, the containers can communicate directly.

Starting the container, the environmental variables file is passed using --env-file:

docker run -p 8080:8080 -d --env-file gameapis.env -d --name gamesapi codebreaker/gamesapi:latest
docker network connect codebreakernet gamesapi

Note

In case you get an error that the container name is already in use, you can stop running containers with docker container stop <containername>. To remove the state of the container, you can use docker container rm <containername> or the docker rm <containername> shorthand notation. To delete the state of all stopped containers, use docker container prune. When starting a new container with dotnet run, you can also add the --rm option to remove a container after exit.

Specifying the name of the gamesapi container, the container is added to the codebreakernet network. Now, we have two containers running communicating with each other, and can play games using the games service.

Let’s create another Docker image, but this time without using a Dockerfile.

Building a Docker image using dotnet publish

In this chapter, we use multiple containers running at the same time. A project we didn’t use in previous chapters is Codebreaker.Bot. This project offers an API and is also a client to the games service – to automatically play games in the background after requested by some API calls. In this section, we’ll build a Docker image for this project – but without creating a Dockerfile first.

Since .NET 7, the dotnet publish command directly supports creating Docker images without a Dockerfile. Using the docker build command, we had to pay attention to some specific .NET behaviors, such as the need to compile multiple projects. With this, it was necessary to specify the context when building the image. The .NET CLI knows about the solutions and project structure. The .NET CLI also knows about the default Docker base images that are used to build an application with ASP.NET Core.

Options to configure the generation can be specified in the project file and using the parameters of dotnet publish. Here are some of the configurations specified in the project file:

<PropertyGroup>
  <ContainerRegistry>codebreaker/bot</ContainerRegistry>
  <ContainerImageTags>3.5.3;latest</ContainerImageTags>
</PropertyGroup>
<ItemGroup>
  <ContainerPort Include="8080" Type="tcp" />
</ItemGroup>

ContainerImageTags and ContainerPort are just two of the elements that are used by dotnet publish. You can change the base image with ContainerBaseImage, specify container runtime identifiers (ContainerRuntimeIdentifier), name the registry (ContainerRegistry), define environmental variables, and more. See https://learn.microsoft.com//dotnet/core/docker/publish-as-container for details.

With dotnet publish, the name of the repository is specified:

cd Codebreaker.Bot
dotnet publish Codebreaker.Bot.csproj --os linux --arch x64 /t:PublishContainer -c Release

The bot API service communicates with the games service, and the games service uses a container running SQL Server. Now, we already have three Docker containers collaborating. The games API needs a connection to the database, and the bot needs a link to the games API.

lock icon The rest of the chapter is locked
Register for a free Packt account to unlock a world of extra content!
A free Packt account unlocks extra newsletters, articles, discounted offers, and much more. Start advancing your knowledge today.
Unlock this book and the full library FREE for 7 days
Get unlimited access to 7000+ expert-authored eBooks and videos courses covering every tech area you can think of
Renews at $19.99/month. Cancel anytime
Banner background image