Microservice Architecture and Containerization using docker are the latest buzzwords in the software industry. But, many people, including me, in the software industry who are developing big monolithic enterprise applications using .NET Framework for many years have a very limited scope of applying these concepts into existing applications. Well, it's not easy to break the enterprise monolithic application into a microservice architecture without redesigning the application. Also, the .NET Core framework would be the de-facto choice for microservice architecture because it supports cross-platform and can be hosted in Linux containers or Windows containers easily. As of today, Windows Docker container does not support GUI applications, such as WinForms, WPF etc. However, we can still consider modernizing .NET Framework monolithic applications by packaging into docker images for automated end-to-end testing or security testing.
In this article, I will explain how to containerize a simple N-Tier CRUD MVC application using docker. We will create a separate app server and database server container images and deploy and run the simple N-Tier MVC application. If you are new to docker, I would recommend you first read this article about
docker for developers and watch the awesome pluralsight course of
Modernizing .NET Apps with Docker by Elton Stoneman.
How it works
I took the
N-Tier Application on ASP.NET MVC - A Complete Solution from the MSDN Code web site that runs on full .NET Framework. This sample application does the basic CRUD operation for maintaining Employees' data using Model-View-Controller pattern with Repository Pattern and N-Tiers Deployment Architecture Pattern. We will modernize this application by containerizing into a docker image. This application will have a separate database and application server instance. The database server will be based on the Docker version of
SQL Server Developer Edition and application server is based on
microsoft/aspnet:latest docker image. Every time a new container instance is created, a new database is created and all the data that was created in the prior container instance is destroyed when the container instance is stopped. This works perfectly for automated testing scenarios.
Steps
Now, I am going to explain about the docker-compose file to orchestrate how to build and deploy a .NET Framework application into a docker container. Visual Studio provides the default container orchestration support for .NET Web Projects. You can add it by right-clicking on the web project and selecting "Container Orchestration Support" as below.
However, I am not using the built-in container orchestration support feature for creating the docker-compose file. I created it manually from scratch using Visual Studio Code Editor.
docker-compose
In the root folder of the project, create a new file called docker-compose.yml with the below code. I used VS Code as my editor because it has great support for yaml file with intelliSense.
- version: '3'
- services:
- docker_ntierdemo_app:
- image: jeevasubburaj/dockerntierdemo_app:v1
- build:
- context: ./NtierMvc/bin/Release/Publish
- depends_on:
- - docker_ntierdemo_db
- hostname: ${APP_UUID}
- container_name: ${APP_UUID}
- networks:
- docker_ntierdemo-net:
- ipv4_address: 172.16.238.20
- docker_ntierdemo_db:
- image: jeevasubburaj/dockerntierdemo_db:v1
- build:
- context: ./Database
- ports:
- - "14333:1433"
- env_file: db_dev.env
- hostname: ${DB_UUID}
- container_name: ${DB_UUID}
- networks:
- docker_ntierdemo-net:
- ipv4_address: 172.16.238.21
- networks:
- docker_ntierdemo-net:
- ipam:
- driver: default
- config:
- - subnet: 172.16.238.0/24
Let's talk about each line in the above docker-compose file to understand what is going on. Before we take a deep dive into that, I would recommend you to read the official
docker-compose guide from the docker website.
This is the version of the docker-compose format that we use in this example.
- services:
- docker_ntierdemo_app:
- ....
- docker_ntierdemo_db:
Service's definition contains the configuration applied to each container started for that service. In our example, we will be creating an application and the Database Server Services.
Before we go into services in detail, let us discuss how to create environment variables in docker-compose using a .env file and custom env files. We are going to create some custom environment variables, such as hostname and SQL Server login password etc. to access it from the docker-compose file.
By default, you can set your environment variables using a .env file which docker-compose automatically looks for. If you want to create a custom environment file, you can also do that and reference that file inside the docker-compose file. In this example, I used both. In addition to that, you can also create the environment variable inside the docker-compose file without creating an environment file.
.env file
- APP_UUID=Demo_App_Server
- DB_UUID=Demo_Db_Server
I have created the custom hostname for both the app and the DB Server, and I will be using these variables inside the docker-compose file. The same value is also configured in web.config so that the app server will be connected to the DB Server.
db_dev.env
- SA_PASSWORD=P@ssw0rd
- ACCEPT_EULA=Y
In this custom environment file, I have defined the default SA Account password and accepted the EULA flag for the SQL Server to start inside the container.
Database Server Services
- image: jeevasubburaj/dockerntierdemo_db:v1
- build:
- context: ./Database
- ports:
- - "14333:1433"
- env_file: db_dev.env
- hostname: ${DB_UUID}
- container_name: ${DB_UUID}
In the first line, I defined the name of the image with version number.
Before we jump into the build section, let us look at other references in that section. I mapped the default SQL port 1433 from the container into 14333 on the host port using ports configuration so that you can connect the database from your host server with servername as localhost,14333. This step is optional only.
We have also defined the hostname and container_name using an environment variable. This will be needed to configure the database server name in our web.config, before we deploy the application into the container.
Build configurations are applied at docker build time. The context configuration defines the path to a directory containing the DockerFile. I created a new folder Database and placed the DockerFile and Database_Setup.sql file and pointing the context to that folder. When we build the docker image using docker-compose, it runs the DockerFile inside the Database Folder and build the database image. By Default, it will look for the file with the name of DockerFile. If you want to create a custom DockerFile Name, you have to add dockerfile configuration to specify the custom docker file name.
DockerFile
- FROM microsoft/mssql-server-windows-developer:latest
- COPY ./Database_Setup.sql .
- RUN sqlcmd -i Database_Setup.sql
This dockerfile gets the base image from the sql server developer edition and coiesy the Database_Setup.sql into the image and executes the SQL query using sqlcmd command which will create the database and the tables defined in the SQL file.
Database_Setup.sql
- USE [master]
- GO
- CREATE DATABASE [NtierMvcDB]
- GO
- USE [NtierMvcDB]
- GO
- CREATE SCHEMA [HR]
- GO
- CREATE TABLE [HR].[Employees]
- (
- [Id] [int] NOT NULL,
- [Name] [nvarchar](50) NOT NULL,
- [Age] [int] NOT NULL,
- [HiringDate] [datetime] NULL,
- [GrossSalary] [decimal](10, 2) NOT NULL,
- [ModifiedDate] [datetime] NOT NULL,
- CONSTRAINT [PK_Employees] PRIMARY KEY CLUSTERED ([Id] ASC) ON [PRIMARY]
- ) ON [PRIMARY]
- GO
- ALTER TABLE [HR].[Employees] ADDCONSTRAINT [DF_Employees_ModifiedDate] DEFAULT (GETDATE()) FOR [ModifiedDate]
- GO
Networks
- networks:
- docker_ntierdemo-net:
- ipam:
- driver: default
- config:
- - subnet: 172.16.238.0/24
In the networks configuration section, we can define any custom network properties that are needed. if we don’t define any networks configuration, docker will create a default network with bridge mode enabled. In the above example, I created custom network with default subnet range so that I can configure the custom ip address for my app and db server. This will be useful for scenarios like when you have some enterprise application with licensing tool installed based on certain device parameters such as mac address, ip address so that you will have the container instances created with the same ip address, mac address every time it's created without installing the license for every instance.
App Server services
- docker_ntierdemo_app:
- image: jeevasubburaj/dockerntierdemo_app:v1
- build:
- context: ./NtierMvc/bin/Release/Publish
- depends_on:
- - docker_ntierdemo_db
- hostname: ${APP_UUID}
- container_name: ${APP_UUID}
- networks:
- docker_ntierdemo-net:
- ipv4_address: 172.16.238.20
In the App Server Services Configuration, we define the name of the image and in the build context, configure the published folder output path. We will create a publish profile from visual studio to deploy the build output in the above mentioned folder along with the DockerFile.
The DockerFile must be added in the project and set the build action as content so that it will also get deployed to the publish folder.
dockerfile
- FROM microsoft/aspnet:latest
- COPY . /inetpub/wwwroot/
In this dockerfile, we are taking the base image of microsoft aspnet docker image and copying the build output directly into the wwwroot folder inside the container image. We can also put the build output into a different folder and create IIS web site using powershell command.
The depends_on configuration defines the dependency between services. In this example, app server is dependent on database server so when we run the service , docker will start the database service first and then it will start the app service based on the order we defined.
Demo
We are now done with the orchestration configuration of deploying our application into docker container using docker compose, and we can now build the image and bring up the container instances to test it. Before we start, we must create the publish profile to deploy the build output into the publish folder. Make sure dockerfile in the web project has build action as content.
Also, change the database server name matching with the db server name defined in env file in web.config file.
Launch the PowerShell window from the root folder and run the docker images command to show the list of images. I have already downloaded aspnet and SQL Server images from the docker hub.
Let's build the docker image using docker-compose build command. This will first create the database image using base sql server developer edison and create the database and tables based on the SQL we provided and then it will create app server based on aspnet framework docker image and copy the build output from publish folder and put it intothe wwwroot folder inside the container image.
Now, that we have successfully created the docker images, we can verify that by running docker images command.
Let us now bring up new container instance from our image using docker-compose up command. This command will create a database container instance first and then app server instance and attach it with the database server. Once the container instances are done we can verify the instance by testing our application from the browser.
Verify the application by launching the browser and putting the ip address of app server container instance.
Now, home page is up and running, let's try adding a new employee into our table.
Let's also verify the data in sql server by connecting with localhost:14333 port from host.
Great. If we stop the container now, all the data that we created will be gone and it will start from a clean slate for next instance. Let us test that by running docker-compose down command. You can also verify if all the running instances are down by running docker ps command.
If we create a new instance now, it will start from clean slate and the employee record that we created should not exist.
Let us run docker-compose up command to bring up the new instance.
We have successfully deployed the complete .N-Tier CRUD MVC application into docker container. As I mentioned earlier, we can use the containerization for automated end to end or security testing for a monolithic application. We can also integrate with CI / CD pipeline to run all the test scenarios before merging the pull request from the feature branch.
Additional Notes
In the above example, we did not store the state changes as part of the container instances. All the changes are gone when the container instance is stopped. However, if we want to store the state of the application and database changes, docker provides the functionality of creating volumes which will mount the folder from host to docker container so that all the state changes will be persisted. This will be useful in the scenario like automated testing to store the results.
In order to create volume in docker, we should use volume configuration section in the docker-compose file. In the example below, I created the directory called DB on my host server and put the MDF and LDF database file inside the folder and then mounted that folder to the container.
The next step is to attach the database instead of creating a database by adding attach_dbs command in env file. This will create a database called NtierMvcDB and attach the existing MDF and LDF file into that every time when the container instance is created. Also, this will store all the DB state changes even after the container is stopped. When we initiate the new container instance, it will show the data from the previous instance as well.
- SA_PASSWORD=P@ssw0rd
- ACCEPT_EULA=Y
- attach_dbs=[{'dbName':'NtierMvcDB','dbFiles':['C:\\\\DB\\\\NtierMvcDB.mdf','C:\\\\DB\\\\NtierMvcDB.ldf']}]
Some of monolithic core application engine may run on windows service. The good thing with docker on windows is, it supports windows service since there is GUI involved. If you want to install your application engine windows service as part of docker image build and run the windows service, use the below powershell commands in DockerFile.
- RUN powershell new-service -Name "AppEngineService" -StartupType Automatic -BinaryPathName "C:\app\bin\AppEngineService.exe"
- RUN powershell start-service -Name "AppEngineService"
Conclusion
I hope this article helps you understand how to containerize the .NET framework monolithic application. Docker containerization is not just only for breaking a monolithic application into microservice architecture. It can also be considered to modernize monolithic application packaging into docker image and ship it very frequently for various scenarios like automated end to end testing, security testing.
I have uploaded the entire source code in my
GitHub repository.