Docker containers offer an isolated and portable environment, enabling developers to seamlessly deploy their .NET applications across various environments, from development machines to testing servers and production infrastructure. This versatility has made them a cornerstone in facilitating .NET development services. In this blog post, we’ll guide you through the complete process of constructing and executing a sample .NET Core console application within a Docker container.
Creating a .NET Core Console App
The first step is to create a basic .NET Core console application that we will later containerize. Open up your terminal or command prompt and run the following commands:
dotnet new console -o SampleApp
cd SampleApp
This will generate a new .NET Core console app project in a directory called SampleApp. Open up the generated SampleApp.csproj file and update the TargetFramework property to target .NET Core 3.1:
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.1</TargetFramework>
</PropertyGroup>
Next, open up the Program.cs file and add a simple “Hello World” message:
using System;
namespace SampleApp
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello World!");
}
}
}
Now you have a basic .NET Core console app that prints “Hello World” to the console when run.
Creating the Dockerfile
The next step is to create a Dockerfile that contains the instructions for building a Docker image for this application. Create an empty file called Dockerfile in the SampleApp directory.
The first line of the Dockerfile specifies which base image our Docker image will be built on top of. For .NET Core apps, the official Microsoft Docker images provide the .NET Core runtime and are a good choice as a base image:
FROM mcr.microsoft.com/dotnet/core/sdk:3.1 AS build-env
The build stage will copy the source code into the container, restore dependencies, compile the code, and publish the output:
WORKDIR /app
COPY . .
RUN dotnet restore
RUN dotnet publish -c Release -o out
After building, we use a second Docker image as a runtime image for the published app files. This keeps the final image lightweight:
FROM mcr.microsoft.com/dotnet/core/runtime:3.1
WORKDIR /app
COPY --from=build-env /app/out .
ENTRYPOINT ["dotnet", "SampleApp.dll"]
The full Dockerfile:
FROM mcr.microsoft.com/dotnet/core/sdk:3.1 AS build-env
WORKDIR /app
COPY . .
RUN dotnet restore
RUN dotnet publish -c Release -o out
FROM mcr.microsoft.com/dotnet/core/runtime:3.1
WORKDIR /app
COPY --from=build-env /app/out .
ENTRYPOINT ["dotnet", "SampleApp.dll"]
Building the Docker Image
Now that we have the Dockerfile ready, we can build the Docker image. Navigate to the folder containing the Dockerfile and run:
docker build -t sampleapp .
This will build a Docker image called “sampleapp” using the Dockerfile in the current directory. Docker will layer the image, restoring dependencies, publishing, and ultimately producing a Docker image containing our .NET application.
Running the Docker Container
To run the Docker container from the newly built image, run:
docker run -it sampleapp
This will start a new container from the sampleapp image, launching the application’s entrypoint which runs dotnet SampleApp.dll. You should see the “Hello World!” output in your terminal.
To run the container in detached mode in the background:
docker run -d --name sampleappcontainer sampleapp
Now the container will run in the background. You can stop, start, or inspect the container using the container name:
docker stop sampleappcontainer
docker start sampleappcontainer
docker logs sampleappcontainer
You have now successfully built and run a .NET Core app inside a Docker container. With Docker, your application is packaged into a standard, portable unit that can be deployed anywhere Docker runs without dependencies on the underlying infrastructure.
Adding Configuration
Often applications require configuration values to be set at runtime. With Docker, these values can be passed into the container as environment variables.
Open Program.cs and modify the main method to read an environment variable:
static void Main(string[] args)
{
var connectionString = Environment.GetEnvironmentVariable("ConnectionString");
Console.WriteLine($"Connection string is {connectionString}");
}
When running the container, pass the value using -e:
docker run -e ConnectionString="server=localhost;database=app;user=sa;password=Passw0rd" sampleappcontainer
Now the app reads the connection string from the environment.
You can also define default configuration values in appsettings.json and override them at runtime. For example:
{
"Logging": {
"LogLevel": {
"Default": "Information"
}
}
}
Reference the settings file in Program.cs:
static void Main(string[] args)
{
var configuration = new ConfigurationBuilder()
.AddJsonFile("appsettings.json")
.Build();
var logLevel = configuration["Logging:LogLevel:Default"];
// log using logLevel
}
Then override LogLevel when running the container:
docker run -e Logging__LogLevel__Default="Debug" sampleappcontainer
Now the app reads the connection string from the environment.
You can also define default configuration values in appsettings.json and override them at runtime. For example:
{
"Logging": {
"LogLevel": {
"Default": "Information"
}
}
Reference the settings file in Program.cs:
static void Main(string[] args)
{
var configuration = new ConfigurationBuilder()
.AddJsonFile("appsettings.json")
.Build();
var logLevel = configuration["Logging:LogLevel:Default"];
// log using logLevel
}
Then override LogLevel when running the container:
docker run -e Logging__LogLevel__Default="Debug" sampleappcontainer
These examples demonstrate how Docker enables flexible configuration of applications at runtime through environment variables.
Docker Compose
For multi-container applications, Docker Compose allows defining and running multiple application containers and their dependencies in a single configuration file.
Create a docker-compose.yml:
version: '3'
services:
app:
image: sampleapp
ports:
- "8080:80"
database:
image: mysql
environment:
- MYSQL_ROOT_PASSWORD=secret
- MYSQL_DATABASE=app
ports:
- "3306:3306"
Now instead of launching individual containers, use Docker Compose:
docker-compose up
This will start both the app and database containers together in the background. The database is only accessible to the app container internally on its container network.
Docker Compose is very useful for testing multi-container setups locally before deployment to production environments. Entire development environments with full application stacks including dependencies can be reproduced consistently.
Building for Production
When deploying to production, you should consider container security and optimizations for performance and size. Here are some best practices:
- Use multi-stage builds to produce a slimmer runtime image from your build artifacts
- Scan images for vulnerabilities with tools like Anchore or Trivy
- Sign and verify images trust with signatures
- Minimize exposed ports and use network policies for security
- Consider a content trust/registries like Docker Trusted Registry
- Use a CI/CD tool like Docker/Kubernetes for automated builds and deployments
For production deployments to a Kubernetes cluster:
- Define deployment, services and other resources in Kubernetes manifests
- Build optimized container images with specific tags for each environment
- Automate rolling deployments of new versions
- Configure resource limits, auto-scaling and health checks
- Implement monitoring, logging and alerting integrations
Containerizing .NET applications with Docker delivers substantial benefits in terms of portability, reproducibility, and scalability across Kubernetes or Docker-powered infrastructure. This approach, coupled with DevOps best practices, unleashes the complete potential for automated and resilient application deployments, making it an enticing proposition for businesses looking to hire .NET developers proficient in leveraging Docker for efficient application deployment strategies.