Recently I had to figure out how to deploy an Asp .Net application on Kubernetes. There were a lot of things to learn and hence I decided to document the process. More importantly this guide is specifically for those using WSL and Minikube.
Also, this could be a great project to add to your resume or portfolio if you're looking to get into DevOps.
This article does not focus much on the concepts of Kubernetes instead it guides you through deploying the application. However, it explains almost every command in detail. The aim is that even a beginner should be able to follow along.
There are tons of resources available on Docker and Kubernetes, please check them out if you're an absolute beginner.
You can also check out these blogs on Docker that I had written previously.
https://heloise.hashnode.dev/running-an-app-via-docker-container
https://heloise.hashnode.dev/multi-container-apps-with-docker-network
https://heloise.hashnode.dev/a-brief-guide-to-docker-volume
Platform used: Windows 10 with WSL2 with Ubuntu
Tools: VsCode, Docker, Minikube & Kubectl
NOTE: Working with WSL could be tricky. There could be some errors while trying out this project. However, most of the errors have already been faced and resolved by others. Googling these could be your best bet.
What is Kubernetes?
Docker is used for creating containers for our application be it frontend, backend or database. An application can have multiple containers, and managing each of these would be an arduous task. Here is where Kubernetes comes in.
In the simplest form, Kubernetes or K8s is a tool that creates and manages Docker Containers.
How does Kubernetes create containers?
The Kubernetes cluster will have one or more nodes. On each of these nodes, the containers are deployed.
The image for the Docker container is specified in a Kubernetes deployment file. This image is then used to start up the containers.
The first step is to build the Docker Images required for the containers. In this example, we need three containers Asp. Net, PostgreSQL and Adminer. We will create a custom Dockerfile for the .Net app container and use the official PostgreSQL, Adminer images for the database.
Next, we should test if our application works by connecting it to the Database container. For this, we have used the docker-compose file.
Once the Docker containers are verified and working, we can deploy on Kubernetes. For this, we first need to create a cluster using Minikube.
Next, we create Kubernetes deployment files for the .Net app and database, using the docker images we just created.
What is Minikube?
Well, minikube is used to deploy the Kubernetes cluster on your local machines.
Once we have minikube running, it will create a single-node cluster. On this node, we can deploy our containers.
Building the Dockerfile
Open WSL terminal
Create a folder called Projects (*optional)
mkdir Projects
Switch to the new folder
cd Projects
Clone sample git repository
git clone https://github.com/ajeetraina/students-database-dotnet-docker
Switch to the cloned folder
cd students-database-dotnet-docker
Open the project in VS Code for ease of editing. You can do this manually or by using the following command
code .
You can use the provided Dockerfile or create it yourself to test your Docker skills!
WORKDIR: sets the current directory where all further commands will be executed
RUN dotnet build -o /app: is a .Net command to create the build files i.e myWebApp.dll
-o specifies the path where the files will be stored
Similarly, the publish command creates files for deployment.
ENV ASPNETCORE_URLS=http://+:5000: we need to set the application URL manually.
FROM mcr.microsoft.com/dotnet/sdk:6.0 #specify the base image to be used COPY . ./src # copy project contents from current dir to src dir WORKDIR /src # sets src dir as the current dir RUN dotnet build -o /app # build app and store output in app dir RUN dotnet publish -o /publish #publish app & store output in publish dir WORKDIR /app #sets app dir as the current dir ENV ASPNETCORE_URLS=http://+:5000 # set the application URL manually EXPOSE 5000 # expose the container port CMD ["./myWebApp"] # command to start the .Net app using dll file
In the WSL terminal build the docker image
docker build . -t students-database-dotnet-docker-app:latest
Open localhost:5000 in the browser, you should see a screen like below
Stop & Delete the running container
#run each command individually docker ps // this will give you the container id docker stop container_id docker rm container_id
Delete the image using rmi command as shown
docker rmi -f students-database-dotnet-docker-app
Create the Docker Compose file
Now that our Dockerfile is correct let us create the Docker Compose file, which includes the database container.
NOTE: I have updated the file as per my understanding you can use it as given.
Database images and setup guide.
Volumes are used for data persistence. Here I have used a named volume.
services: db: # Databse container name image: postgres # container image restart: always environment: POSTGRES_PASSWORD: example volumes: - postgres-data:/var/lib/postgresql/data adminer: # Databse management container name image: adminer restart: always ports: - 8080:8080 #host port:conatiner port app: # Application container name build: ./ #specify path to Dockerfile ports: - 5000:5000 #host port:conatiner port depends_on: - db volumes: postgres-data:
Create the containers in the compose file using the following command, where -d runs the container in detached mode
docker compose up -d
NOTE: First open localhost:5000 this creates the database my_db then open localhost:8080 and login with the credentials.
These credentials are specified in the appsettings.json file.
{ "Logging": { "LogLevel": { "Default": "Information", "Microsoft": "Warning", "Microsoft.Hosting.Lifetime": "Information" } }, "AllowedHosts": "*", "ConnectionStrings": { "SchoolContext": "Host=db;Database=my_db;Username=postgres;Password=example" } }
Open localhost:5000 in the browser, you should see a screen like below
Open localhost:8080
Once you have logged in, this screen will be visible. Here you can add student records, and the name will be displayed here localhost:5000
Stop the containers
docker compose down
Delete images like so
#run each command individually docker rmi -f students-database-dotnet-docker-app docker rmi -f adminer docker rmi -f postgres
Deploy on Kubernetes using Minikube
NOTE: We won't be using any Docker Image Repository here. It is relatively easy and is also common practice. If using an Image from the repository, specify the Image URL in Container Specification in the app deployment file.
Overview when using an Image registry (eg. Docker Hub)
1. Build image
2. Create Docker Hub Account
3. Login to docker hub from cli
4. Push image to docker hub
5. Run docker container using image from docker hub (to test if all works fine)
6. In Kubernetes deployment file specify this same image in the container specification section
Start Minikube Cluster
minikube start
Point the local Docker daemon to the minikube internal Docker registry. This is so that we can use a locally built Docker image in the deployment file. As mentioned above you can opt for using some docker image registry instead.
(This step will be omitted in case you use an image registry.)
eval $(minikube -p minikube docker-env)
Build the docker image. This image will be used in the app deployment file.
(This step will also be omitted in case you use an image registry.)
docker build . -t students-database-dotnet-docker-app:latest
Create the app-deploy.yml file as shown.
Refer here for Kubernetes deployment syntax & Service syntax. Generally, we refer to K8s documentation for the format and modify images, container names, ports, replicas or any other relevant details in the sample YAML file.
Also, another good practice is to have the deployment and service in a single YAML file.
apiVersion: apps/v1 kind: Deployment metadata: name: app-deploy # name of deployment namespace: default spec: replicas: 1 # specify number of replicas selector: matchLabels: app: app # this selector label should be the same accross the deployment and service files. template: metadata: labels: app: app # same selector label as deployment spec: # container specifications containers: - name: students-database-dotnet-docker-app image: students-database-dotnet-docker-app:latest imagePullPolicy: IfNotPresent ports: - containerPort: 5000 --- # separates deployment and service apiVersion: v1 kind: Service metadata: name: app-service # name of service spec: type: NodePort # since this service has to be accessible externally selector: app: app # same selector label as deployment ports: - nodePort: 30001 # any port in the range 30000-32768 port: 5000 #container port targetPort: 5000 # A Service can map any incoming port to a targetPort. By default and for convenience, the targetPort is set to the same value as the port field.
Observe how we have used the locally built image for the container specification. Also, specify the imagePullPolicy as IfNotPresent (pulls the image from the repository only if it's not available locally)
NodePort: used when a service needs to be accessed externally we specify the type as NodePort
Similarly, create the other deployment files.
db-deploy.yaml
apiVersion: apps/v1 kind: Deployment metadata: name: db-deploy namespace: default spec: replicas: 1 selector: matchLabels: app: pgdb template: metadata: labels: app: pgdb spec: containers: - name: postgres image: postgres env: - name: POSTGRES_PASSWORD value: example ports: - containerPort: 5432 --- apiVersion: v1 kind: Service metadata: name: db-service spec: # default type ClusterIP is used since database should not be accessible externally selector: app: pgdb ports: - port: 5432 targetPort: 5432
admin-db-deploy.yaml
apiVersion: apps/v1 kind: Deployment metadata: name: admin-deploy namespace: default spec: replicas: 1 selector: matchLabels: app: admin template: metadata: labels: app: admin spec: containers: - name: admin image: adminer ports: - containerPort: 8080 --- apiVersion: v1 kind: Service metadata: name: admin-service spec: type: NodePort selector: app: admin ports: - protocol: TCP nodePort: 30002 port: 8080 targetPort: 8080
Now the Database node is exposed via the service, hence this service name db-service should be used wherever we need to connect to the Database like in the appsettings.json file and while logging in to Adminer portal.
Updated appsettings.json file code
{ "Logging": { "LogLevel": { "Default": "Information", "Microsoft": "Warning", "Microsoft.Hosting.Lifetime": "Information" } }, "AllowedHosts": "*", "ConnectionStrings": { "SchoolContext": "Host=db-service;Database=my_db;Username=postgres;Password=example" } }
In the terminal run, the kubectl apply command to create the pods and services
#run each command individually minikube kubectl -- apply -f db-deploy.yaml minikube kubectl -- apply -f admin-db-deploy.yaml minikube kubectl -- apply -f app-deploy.yaml
Check if pods are created
minikube kubectl -- get pods
The status should be running. This may take some time.
Check if services are created
minikube kubectl -- get svc
Now to access the application on the browser we need to fetch Minikube IP and service NodePort
minikube service app-service --url
*Do not cancel this operation as long as you need to access it in the browser.
Access the application using the IP address(http://127.0.0.1:33291). Once you do this, the database "my_db" is created.
Now let us access the database on the browser. In a new terminal run the following
minikube service admin-service --url
Open the browser to this IP and log in as shown.
*Server will be the database service name used in the appsettings.json file i.e db-service
You can now add records to the table, that will be seen on the web app.
Refresh the application at http://127.0.0.1:33291. You should see the name you added to the table.
Congratulations!!! You've deployed an ASP. Net app on Kubernetes.
Make sure to delete the services and pods once you have finished
#run each command individually minikube kubectl -- delete -f db-deploy.yaml minikube kubectl -- delete -f admin-db-deploy.yaml minikube kubectl -- delete -f app-deploy.yaml
Ideally, we would add an Ingress service and controller. I have created the Ingress resource which is available in the repository mentioned below. If you are using Minikube on windows or mac this won't be an issue. You can set up ingress as given here.
However, accessing ingress via Minikube and WSL is a hurdle. There are a couple of workarounds which I've tried. But they seemed beyond the scope of this article and it's still a work in progress.
Adding ingress is optional in this case. So you could try out a similar approach to other projects and applications.
Happy learning!!