Building Multi-Container .NET App using Docker Desktop
source link: https://www.docker.com/blog/building-multi-container-net-app-using-docker-desktop/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
Building Multi-Container .NET App using Docker Desktop
Ajeet Singh RainaApr 12 2022
.NET is a free, open-source development platform for building numerous apps, such as web apps, web APIs, serverless functions in the cloud, mobile apps and much more. .NET is a general purpose development platform maintained by Microsoft and the .NET community on GitHub. It is cross-platform, supporting Windows, macOS and Linux, and can be used in device, cloud, and embedded/IoT scenarios.
Docker is quite popular among the .NET community. .NET Core can easily run in a Docker container. .NET has several capabilities that make development easier, including automatic memory management, (runtime) generic types, reflection, asynchrony, concurrency, and native interop. Millions of developers take advantage of these capabilities to efficiently build high-quality applications.
Building the Application
In this tutorial, you will see how to containerize a .NET application using Docker Compose. The application used in this blog is a Webapp communicating with a Postgresql database. When the page is loaded, it will query the Student table for the record with ID and display the name of student on the page.
What will you need?
Getting Started
Visit https://www.docker.com/get-started/ to download Docker Desktop for Mac and install it in your system.
Once the installation gets completed, click “About Docker Desktop” to verify the version of Docker running on your system.
If you follow the above steps, you will always find the latest version of Docker desktop installed on your system.
1. In your terminal, type the following command
dotnet new webApp -o myWebApp --no-https
The `dotnet new` command creates a .NET project or other artifacts based on a template.
You should see the output in terminal
The template ASP.NET Core Web App was created successfully.
This template contains technologies from parties other than Microsoft, see https:
//aka
.ms
/aspnetcore/6
.0-third-party-notices
for
details.
This will bootstrap a new web application from a template shipped with dotnet sdk. The -o parameter creates a directory named myWebApp where your app is stored.
2. Navigate to the application directory
cd myWebApp
you will have a list of files –
tree -L 2
.
├── Pages
│ ├── Error.cshtml
│ ├── Error.cshtml.cs
│ ├── Index.cshtml
│ ├── Index.cshtml.cs
│ ├── Privacy.cshtml
│ ├── Privacy.cshtml.cs
│ ├── Shared
│ ├── _ViewImports.cshtml
│ └── _ViewStart.cshtml
├── Program.cs
├── Properties
│ └── launchSettings.json
├── appsettings.Development.json
├── appsettings.json
├── myWebApp.csproj
├── obj
│ ├── myWebApp.csproj.nuget.dgspec.json
│ ├── myWebApp.csproj.nuget.g.props
│ ├── myWebApp.csproj.nuget.g.targets
│ ├── project.assets.json
│ └── project.nuget.cache
└── wwwroot
├── css
├── favicon.ico
├── js
└── lib
8 directories, 19 files
3. In your terminal, type the following command to run your application
The dotnet run
command provides a convenient option to run your application from the source code.
dotnet run
The application will start to listen on port 5000
for requests
# dotnet run
Building...
warn: Microsoft.AspNetCore.DataProtection.Repositories.FileSystemXmlRepository[60]
Storing keys
in
a directory
'/root/.aspnet/DataProtection-Keys'
that may not be persisted outside of the container. Protected data will be unavailable when the container is destroyed.
warn: Microsoft.AspNetCore.Server.Kestrel[0]
Unable to bind to http:
//localhost
:5000 on the IPv6 loopback interface:
'Cannot assign requested address'
.
info: Microsoft.Hosting.Lifetime[0]
Now listening on: http:
//localhost
:5000
info: Microsoft.Hosting.Lifetime[0]
Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
Hosting environment: Development
info: Microsoft.Hosting.Lifetime[0]
Content root path:
/src
4. Test the application
Run the curl
command to test the connection of the web application.
# curl http://localhost:5000
5. Put the application in the container
In order to run the same application in a Docker container, let us create a Dockerfile with the following content:
FROM mcr.microsoft.com/dotnet/sdk as build
COPY . ./src
WORKDIR /src
RUN dotnet build -o /app
RUN dotnet publish -o /publish
FROM mcr.microsoft.com/dotnet/aspnet as base
COPY --from=build /publish /app
WORKDIR /app
EXPOSE
80
CMD
[
"./myWebApp"
]
This is a Multistage Dockerfile. The build stage uses SDK images to build the application and create final artifacts in the publish folder. Then in the final stage copy artifacts from the build stage to the app folder, expose port 80 to incoming requests and specify the command to run the application myWebApp.
Now that we have defined everything we need to run in our Dockerfile, we can now build an image using this file. In order to do that, we’ll need to run the following command:
$ docker build -t mywebapp
We can now verify that our image exists on our machine by using docker images command:
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
mywebapp latest 6acc7ebf3a1d 25 seconds ago 210MB
In order to run this newly created image, we can use the docker run command and specify the ports that we want to map to and the image we wish to run.
$ docker run --rm - p 5000:80 mywebapp
- p 5000:80
– This exposes our application which is running on port 80 within our container on http://localhost:5000 on our local machine.--rm
– This flag will clean the container after it runsmyweapp
– This is the name of the image that we want to run in a container.
Now we start the browser and put http://localhost:5000 to address bar
Update application
The myWebApp and Postgresql will be running in two separate containers, and thus making this a multi-container application.
1. Add package to allow app talk to database
Change directory to myWebapp and run the following command:dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL
2. Create student model
- Create a Models folder in the project folder
- Create Models/Student.cs with the following code:
using
System;
using
System.Collections.Generic;
namespace
myWebApp.Models
{
public
class
Student
{
public
int
ID {
get
;
set
; }
public
string
LastName {
get
;
set
; }
public
string
FirstMidName {
get
;
set
; }
public
DateTime EnrollmentDate {
get
;
set
; }
}
}
3. Create the `SchoolContext` with the following code:
using
Microsoft.EntityFrameworkCore;
namespace
myWebApp.Data
{
public
class
SchoolContext : DbContext
{
public
SchoolContext (DbContextOptions<SchoolContext> options) :
base
(options)
{
}
public
DbSet<Models.Student> Students {
get
;
set
; }
protected
override
void
OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Models.Student>().ToTable(
"Student"
);
}
}
}
4. Register SchoolContext
to DI in Startup.cs
public
void
ConfigureServices(IServiceCollection services)
{
services.AddDbContext<myWebApp.Data.SchoolContext>(options =>
options.UseNpgsql(Configuration.GetConnectionString(
"SchoolContext"
)));
services.AddRazorPages();
}
5. Adding database connection string to `appsettings.json`
{
"Logging"
: {
"LogLevel"
: {
"Default"
:
"Information"
,
"Microsoft"
:
"Warning"
,
"Microsoft.Hosting.Lifetime"
:
"Information"
}
},
"AllowedHosts"
:
"*"
,
"ConnectionStrings"
: {
"SchoolContext"
: jo online
"Host=db;Database=my_db;Username=postgres;Password=example"
}
}
6. Bootstrap the table if it does not exist in Program.cs
public
static
void
Main(
string
[] args)
{
var
host= CreateHostBuilder(args).Build();
CreateDbIfNotExists(host);
host.Run();
}
private
static
void
CreateDbIfNotExists(IHost host)
{
using
(
var
scope = host.Services.CreateScope())
{
var
services = scope.ServiceProvider;
try
{
var
context = services.GetRequiredService<Data.SchoolContext>();
context.Database.EnsureCreated();
// DbInitializer.Initialize(context);
}
catch
(Exception ex)
{
var
logger = services.GetRequiredService<ILogger<Program>>();
logger.LogError(ex,
"An error occurred creating the DB."
);
}
}
}
Update the UI
Index.cshtml
<
div
class
=
"row mb-auto"
>
<
p
>Student Name is @Model.StudentName</
p
>
</
div
>
and Index.cshtml.cs
public
class
IndexModel : PageModel
{
public
string
StudentName {
get
;
private
set
; } =
"PageModel in C#"
;
private
readonly
ILogger<IndexModel> _logger;
private
readonly
myWebApp.Data.SchoolContext _context;
public
IndexModel(ILogger<IndexModel> logger, myWebApp.Data.SchoolContext context)
{
_logger = logger;
_context= context;
}
public
void
OnGet()
{
var
s =_context.Students.Where(d=>d.ID==1).FirstOrDefault();
this
.StudentName = $
"{s?.FirstMidName} {s?.LastName}"
;
}
}
Configuration file
The entry point to Docker Compose is a Compose file, usually called docker-compose.yml
In the project directory, create a new file docker-compose.yml
in it. Add the following contents:
services:
db:
image:
postgres
restart:
always
environment:
POSTGRES_PASSWORD:
example
volumes:
-
postgres-data
:
/var/lib/postgresql/data
adminer:
image:
adminer
restart:
always
ports:
-
8080
:
8080
app:
build:
context:
.
dockerfile:
./Dockerfile
ports:
-
5000
:
80
depends_on:
-
db
volumes:
postgres-data:
In this Compose file:
- Two services in this Compose are defined by the name
db
andweb
attributes; the adminer service is a helper for us to access db - Image name for each service defined using
image
attribute - The
postgres
image starts the Postgres server. environment
attribute defines environment variables to initialize postgres server.POSTGRES_PASSWORD
is used to set the default user’s, postgres, password. This user will be granted superuser permissions for the database my_db in the connectionstring.
- app application uses the
db
service as specified in the connection string - The app image is built using the Dockerfile in the project directory
- Port forwarding is achieved using
ports
attribute. depends_on
attribute allows to express dependency between services. In this case, Postgres will be started before the app. Application-level health checks are still the user’s responsibility.
Start the application
All services in the application can be started, in detached mode, by giving the command:
docker-compose up -d
An alternate Compose file name can be specified using -f
option.
An alternate directory where the compose file exists can be specified using -p
option.
This shows the output as:
docker-compose up -d
Starting mywebapp_adminer_1 ...
done
Starting mywebapp_db_1 ...
done
Starting mywebapp_app_1 ...
done
The output may differ slightly if the images are downloaded as well.
Started services can be verified using the command docker-compose ps:
docker-compose
ps
Name Command State Ports
------------------------------------------------------------------------------------
mywebapp_adminer_1 entrypoint.sh docker-php-e ... Up 0.0.0.0:8080->8080
/tcp
mywebapp_app_1 .
/myWebApp
Up 0.0.0.0:5000->80
/tcp
mywebapp_db_1 docker-entrypoint.sh postgres Up 5432
/tcp
This provides a consolidated view of all the services, and containers within each of them.
Alternatively, the containers in this application, and any additional containers running on this Docker host can be verified by using the usual docker container ls
command
docker container
ls
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
ee35a9399b80 mywebapp_app
"./myWebApp"
29 minutes ago Up About a minute 0.0.0.0:5000->80
/tcp
mywebapp_app_1
0fc85278791c postgres
"docker-entrypoint.s…"
30 minutes ago Up About a minute 5432
/tcp
mywebapp_db_1
a9c725d0e684 adminer
"entrypoint.sh docke…"
30 minutes ago Up About a minute 0.0.0.0:8080->8080
/tcp
mywebapp_adminer_1
Service logs can be seen using docker-compose logs
command, and looks like:
docker container logs mywebapp_db_1
PostgreSQL Database directory appears to contain a database; Skipping initialization
2021-03-16 04:19:51.862 UTC [1] LOG: starting PostgreSQL 13.2 (Debian 13.2-1.pgdg100+1) on x86_64-pc-linux-gnu, compiled by
gcc
(Debian 8.3.0-6) 8.3.0, 64-bit
2021-03-16 04:19:51.863 UTC [1] LOG: listening on IPv4 address
"0.0.0.0"
, port 5432
2021-03-16 04:19:51.863 UTC [1] LOG: listening on IPv6 address
"::"
, port 5432
2021-03-16 04:19:51.868 UTC [1] LOG: listening on Unix socket
"/var/run/postgresql/.s.PGSQL.5432"
2021-03-16 04:19:51.875 UTC [29] LOG: database system was shut down at 2021-03-16 04:19:04 UTC
2021-03-16 04:19:51.884 UTC [1] LOG: database system is ready to accept connections
2021-03-16 04:20:03.442 UTC [1] LOG: received fast
shutdown
request
2021-03-16 04:20:03.444 UTC [1] LOG: aborting any active transactions
2021-03-16 04:20:03.446 UTC [1] LOG: background worker
"logical replication launcher"
(PID 35) exited with
exit
code 1
2021-03-16 04:20:03.447 UTC [30] LOG: shutting down
2021-03-16 04:20:03.473 UTC [1] LOG: database system is shut down
PostgreSQL Database directory appears to contain a database; Skipping initialization
2021-03-16 04:20:53.597 UTC [1] LOG: starting PostgreSQL 13.2 (Debian 13.2-1.pgdg100+1) on x86_64-pc-linux-gnu, compiled by
gcc
(Debian 8.3.0-6) 8.3.0, 64-bit
2021-03-16 04:20:53.597 UTC [1] LOG: listening on IPv4 address
"0.0.0.0"
, port 5432
2021-03-16 04:20:53.597 UTC [1] LOG: listening on IPv6 address
"::"
, port 5432
2021-03-16 04:20:53.601 UTC [1] LOG: listening on Unix socket
"/var/run/postgresql/.s.PGSQL.5432"
2021-03-16 04:20:53.606 UTC [26] LOG: database system was shut down at 2021-03-16 04:20:03 UTC
2021-03-16 04:20:53.618 UTC [1] LOG: database system is ready to accept connections
2021-03-16 04:21:31.054 UTC [38] ERROR: invalid input syntax
for
type
timestamp:
""
at character 91
2021-03-16 04:21:31.054 UTC [38] STATEMENT: INSERT INTO
"Student"
(
"LastName"
,
"FirstMidName"
,
"EnrollmentDate"
)
VALUES (
'YHH'
,
'HH'
,
''
)
2021-03-16 04:33:09.323 UTC [1] LOG: received fast
shutdown
request
2021-03-16 04:33:09.325 UTC [1] LOG: aborting any active transactions
2021-03-16 04:33:09.327 UTC [1] LOG: background worker
"logical replication launcher"
(PID 32) exited with
exit
code 1
2021-03-16 04:33:09.329 UTC [27] LOG: shutting down
2021-03-16 04:33:09.342 UTC [1] LOG: database system is shut down
PostgreSQL Database directory appears to contain a database; Skipping initialization
2021-03-16 04:49:23.844 UTC [1] LOG: starting PostgreSQL 13.2 (Debian 13.2-1.pgdg100+1) on x86_64-pc-linux-gnu, compiled by
gcc
(Debian 8.3.0-6) 8.3.0, 64-bit
2021-03-16 04:49:23.844 UTC [1] LOG: listening on IPv4 address
"0.0.0.0"
, port 5432
2021-03-16 04:49:23.844 UTC [1] LOG: listening on IPv6 address
"::"
, port 5432
2021-03-16 04:49:23.849 UTC [1] LOG: listening on Unix socket
"/var/run/postgresql/.s.PGSQL.5432"
2021-03-16 04:49:23.855 UTC [26] LOG: database system was shut down at 2021-03-16 04:33:09 UTC
2021-03-16 04:49:23.862 UTC [1] LOG: database system is ready to accept connections
Verify application
Let’s access the application. In your browser address bar type http://localhost:5000
you will see the page show no student name since the database is empty.
Open a new tab with address http://localhost:8080 and you will be asked to login:
Use postgres and example as username/password to login my_db
. Once you are logged in, you can create a new student record as shown:
Next, refresh the app page at http://localhost:5000, the new added student name will be displayed:
Shutdown application
Shutdown the application using docker-compose down:
docker-compose down
Stopping mywebapp_app_1 ...
done
Stopping mywebapp_db_1 ...
done
Stopping mywebapp_adminer_1 ...
done
Removing mywebapp_app_1 ...
done
Removing mywebapp_db_1 ...
done
Removing mywebapp_adminer_1 ...
done
Removing network mywebapp_default
This stops the container in each service and removes all the services. It also deletes any networks that were created as part of this application.
Conclusion
We demonstrated the containerization of .NET application and the usage of docker compose to construct a two layers simple web application with dotnet. The real world business application can be composed of multiple similar applications, ie. microservice application, that can be described by docker compose file. The same process in the tutorial can be applied to much more complicated applications.
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK