Containerize a .Net application with Docker: A comprehensive guide

Bob Code
29 min readAug 13, 2024

--

Introduction

Docker allows you to create applications that are isolated from your underlying infrastructure.

This isolation makes it easy to move your applications between different hosts without encountering issues

Let’s look at how Docker works!

Docker Components

Docker has three main components:

  • Client
  • Host
  • Registry
Local Docker Flow — Bob Code

Locally, the client is your command line, with this you send commands (APIs) to the Host.

The Host locally is your Docker Desktop, which manages and creates containers and images.

Docker registry is where images can be fetched from

Docker Host manages two things (simplifying):

  • Containers
  • Images

Images are read-only containers, containers are instances of an image.

How to create a container?

To build a container, we can either start from scratch or start with an image.

What is an image?

It is a standardised package that includes all the binaries, configs and other dependencies of your base app. It islike a blueprint to build upon.

For instance, there is already an image that has the .Net runtime so you don’t have to do it yourself.

Docker images are built in layers, where each layer represents a step in your Dockerfile.

https://www.linkedin.com/pulse/understanding-docker-layers-efficient-image-building-majid-sheikh#

Let’s look at an example, we want to create a new .Net app

Layers:

  • Base Image: .Net base image
  • Layer A: Add/ Restore dependencies
  • Layer B: Add application code
  • Layer C: Build and publish your app
  • Layer D: Specify entry point of the app

We will look deeper into each step later ;)

How to run a container from an image?

Now that we have an image ready, we can run (create) a container from it.

A container is essentially an isolated operating system process with its own filesystem, networking, and process tree, all separate from the host machine.

The container gets its executable from the image configuration, a container is basically a running instance of an image.

That also means that we can build many more containers of the same image!

In order to create our container based on an image we can use the command “Run” (more on this later!)

https://www.r-bloggers.com/2019/02/running-your-r-script-in-docker/

Where do we get the base image?

This is where repositories like Docker Hub come in handy.

There you will find an image for — almost — all your development needs!

Now that we get the basics, let’s create our very first container

Creating your first container

Here’s the process:

  • Get Docker Desktop
  • Get an image
  • Create a container

Install Docker

  • Install docker desktop (This will be your host locally)
  • Ensure Docker Desktop is running. Avoid pausing or quitting the application while using Docker
  • Check
docker --version

Get an image

Find image via docker hub, then run command

docker pull [nameofyourimage]

e.g.

docker pull hello-world

Display all images

docker images

You can see that each image has an ID

The tag indicates which version of the image you get

Create a container

Create a container from an image by running: docker run + image name

docker run nameofyourimage

e.g.

docker run hello-world

What happens in the background?

  • The Docker client contacts the Docker daemon.
  • The Docker daemon either pulls the “hello-world” image from the Docker Hub OR gets the one you have locally
  • The Docker daemon creates a new container from that image
Docker run flow https://www.geeksforgeeks.org/features-of-docker/

Great, we just ran our first container! Let’s now delve deeper

Agenda

Docker commands

  • Docker command process
  • Pull images
  • Build images
  • Display containers/ images
  • Run your container
  • Map host and container port
  • How not to get into the Docker CLI when doing Docker Run?
  • Going into the container and running commands
  • How to tag Docker containers?
  • Stop, start, restart and kill containers
  • Remove containers
  • Remove images

Creating a .Net Dockerfile

  • How to create a Dockerfile
  • .Net Dockerfile process overview
  • Creating a .Net Dockerfile manually
  • Using the Default Dockerfile

Dockerfile commands explained

  • FROM
  • WORKDIR
  • Relative vs absolute paths
  • COPY
  • ARG and ENV
  • EXPOSE
  • RUN
  • ENTRYPOINT

Further topics

  • Volumes
  • DockerHub
  • Azure
  • Docker Hub
  • Default Docker File Explained

Docker Commands Reference Table

Commands used in this blog

+---------------------+-------------------------------------------------------------------------------------+-----------------------------------------------------+
| Command | Description | Example |
+---------------------+-------------------------------------------------------------------------------------+-----------------------------------------------------+
| FROM | Specifies the base image to use for the Docker image. | FROM mcr.microsoft.com/dotnet/sdk:7.0 |
| | Can be tagged for specific versions. | FROM mcr.microsoft.com/dotnet/sdk:7.0 AS base |
+---------------------+-------------------------------------------------------------------------------------+-----------------------------------------------------+
| WORKDIR | Sets the working directory inside the container. | WORKDIR /src |
| | Creates the directory if it doesn’t exist. | WORKDIR /app |
+---------------------+-------------------------------------------------------------------------------------+-----------------------------------------------------+
| COPY | Copies files or directories from the source location on the host to the target | COPY ./samplewebapp/ /usr/share/nginx/html |
| | location in the container. | COPY ./myapp/ /app/myapp/ |
+---------------------+-------------------------------------------------------------------------------------+-----------------------------------------------------+
| COPY --from | Copies files from another image defined in a multi-stage build. | COPY --from=base /app /src/ |
| | Reuses built artifacts to optimize images. | COPY --from=builder /app/build /app |
+---------------------+-------------------------------------------------------------------------------------+-----------------------------------------------------+
| ARG | Defines a variable that users can pass at build-time to the Dockerfile. | ARG build_configuration=Release |
| | Can be used for dynamic configurations during image build. | ARG app_version=1.0 |
+---------------------+-------------------------------------------------------------------------------------+-----------------------------------------------------+
| ENV | Sets an environment variable that can be used at runtime. | ENV APP_HOME /app |
| | Useful for configuring applications in the container. | ENV NODE_ENV production |
+---------------------+-------------------------------------------------------------------------------------+-----------------------------------------------------+
| EXPOSE | Informs Docker that the container listens on the specified network ports at runtime. | EXPOSE 80 |
| | Documents the ports intended to be published. | EXPOSE 443 |
+---------------------+-------------------------------------------------------------------------------------+-----------------------------------------------------+
| RUN | Executes a command in a new layer on top of the current image and commits the | RUN dotnet build "NameOfYourProject.csproj" |
| | results. | RUN apt-get update && apt-get install -y curl |
+---------------------+-------------------------------------------------------------------------------------+-----------------------------------------------------+
| ENTRYPOINT | Configures a container that will run as an executable. | ENTRYPOINT ["dotnet", "/app/MyApplication.dll"] |
| | Can be used to specify a command that runs when the container starts. | ENTRYPOINT ["/usr/bin/nginx", "-g", "daemon off;"] |
+---------------------+-------------------------------------------------------------------------------------+-----------------------------------------------------+
| docker login | Authenticates your Docker client to your Docker Hub account. | docker login --username yourusername |
| | Prompts for password if not provided. | docker login -u yourusername -p yourpassword |
+---------------------+-------------------------------------------------------------------------------------+-----------------------------------------------------+
| docker tag | Tags an image to a repository for organization and versioning. | docker tag 255 bobby12548741/dotnetdocker:1.0.3 |
| | Creates a new tag for an existing image. | docker tag myimage:latest myimage:v1.0 |
+---------------------+-------------------------------------------------------------------------------------+-----------------------------------------------------+
| docker push | Uploads an image to a Docker Hub repository. | docker push bobby12548741/dotnetdocker:1.0.3 |
| | Shares images for collaboration or deployment. | docker push myimage:v1.0 |
+---------------------+-------------------------------------------------------------------------------------+-----------------------------------------------------+
| docker pull | Downloads an image from a Docker Hub repository. | docker pull bobby12548741/dotnetdocker:1.0.3 |
| | Retrieves the latest version if no tag is specified. | docker pull nginx |
+---------------------+-------------------------------------------------------------------------------------+-----------------------------------------------------+
| docker images | Lists all the images on the local machine. | docker images |
| | Displays details like repository, tag, and image ID. | docker images -a |
+---------------------+-------------------------------------------------------------------------------------+-----------------------------------------------------+
| docker rmi | Removes one or more images from the local machine. | docker rmi [IMAGE ID] |
| | Force removal can be done using the -f flag. | docker rmi -f myimage:latest |
+---------------------+-------------------------------------------------------------------------------------+-----------------------------------------------------+
| docker run | Creates and starts a container from a specified image. | docker run --name myfavouriteapp -p 9000:80 samplewebapp |
| | Additional options can specify ports and volumes. | docker run -d -p 8080:80 nginx |
+---------------------+-------------------------------------------------------------------------------------+-----------------------------------------------------+
| docker exec | Runs a command in a running container. | docker exec -it [container_id] bash |
| | Used for debugging or running additional commands in a container. | docker exec -it mycontainer /bin/sh |
+---------------------+-------------------------------------------------------------------------------------+-----------------------------------------------------+
| docker volume | Manages Docker volumes for persistent storage. | docker volume create my_volume |
| | Useful for storing data that needs to persist beyond container lifecycle. | docker volume rm my_volume |
+---------------------+-------------------------------------------------------------------------------------+-----------------------------------------------------+
| docker run -v | Mounts a volume from the host machine into the container. | docker run -d -v C:\Users\Devoteam\Desktop\docker\backup:/usr/share/nginx/dockerfiles --name webapp -p 9100:80 samplewebapp:1.0.2 |
| | Allows sharing of data between host and container. | docker run -v /host/path:/container/path myimage |
+---------------------+-------------------------------------------------------------------------------------+-----------------------------------------------------+

Docker client commands

To effectively use Docker, the first step is to understand the Docker Client and the commands (Client APIs) it can send to the Docker Daemon (Host), which in turn executes these commands.

Locally, these commands can be run using CLI tools like PowerShell or Terminal.

Docker command process

Let’s explore the process of creating a Docker container using CLI commands, focusing on three fundamental commands:

  • docker build: Create an image
  • docker pull: Fetch an image
  • docker run: Run a container
https://nickjanetakis.com/blog/understanding-how-the-docker-daemon-and-docker-cli-work-together

And now with additional commands

  • In Blue: Docker components
  • In Yellow: CLI commands
  • In Black: CLI commands that remove container/ image
  • In Red: CLI commands that stop container
Bob Code: Docker Commands Flow https://miro.com/app/board/uXjVKrXFv4A=/?utm_source=showme&utm_campaign=cpa&utm_content=graph
  • docker pull: This command fetches an image from a Docker registry, such as Docker Hub, and stores it locally
  • docker build: create your image from a Dockerfile
  • docker run: create your container based on an image
  • docker stop: stop container
  • docker restart: stop and start container
  • docker kill: abruptly stop your container
  • docker rm: remove your container
  • docker rmi:remove your image
https://learn.microsoft.com/en-us/dotnet/architecture/microservices/docker-application-development-process/docker-app-development-workflow

Pull images

Browse Docker Hub to see the official .Net images

https://hub.docker.com/r/microsoft/dotnet

Get the .Net SDK by running “docker pull nameofrepo/nameofimage”

docker pull mcr.microsoft.com/dotnet/sdk

To specify a .Net version, you can target the tag by doing “docker pull nameofrepo/nameofimage:versiontag”

docker pull mcr.microsoft.com/dotnet/sdk:6.0

See the current (2024) official tags:

Build images

You can either build your own image from scratch or on top of a base image pulled from a registry.

To do so, you will need to create a Dockerfile (see Dockerfile section) which will include the commands to create the image.

Images management https://www.tutorialspoint.com/docker/docker_hub.htm

Once your image is ready you can simply invoke the command “build”

Go to the folder where your Dockerfile is located

docker build -t samplewebapp:1.0.0 .
  • docker build

the command to build the image

  • -t samplewebapp:1.0.0

“t” for tag, samplewebapp the repository name, 1.0.0 the tag name

if no tag name are given, by default it will create that repo with the tag of latest

  • “.”

indicates where our Dockerfile is, the dot indicates the current folder

A mistake in your Dockerfile

Error

ERROR [internal] load metadata for docker.io/library/ngnix:latest

Solution

That means that there is a mistake in your Dockerfile!

After building succesfully your image you should get

To explicitely define where the Dockerfile is located — if not in the current directory — use “-f”

The “.” at the end indicates the current directory — It is however recommended to navigate to the directory

docker build -f directory\folder\dockerfilename -q -t yourappname:yourtag .

Display containers & images

After creating an image, it’s a good practice to verify its existence. You can use the following commands to display containers and images

  • Display all images
docker images
  • Display all containers
docker ps -a

You can see that docker automatically assigns names to containers (e.g. epic_wescoff)

  • Display only active containers
docker ps

Run (create) your container

If the image name exists in docker hub and not locally, it will download the image and create a container from it

docker run -d nameofimage

e.g.

docker run -d ubuntu

The -d flag (detached mode) allows you to run the container in the background, freeing up your CLI for other tasks.

“ — name” will specify the name of your container

docker run --name myfavouritecontainer -d myfavouriteimage
We can keep writing CLI commands (otherwise we would be inside the container)

Now the terminal keeps out of the running container

  • -e

To add environment variables when running the app, just use “-e”

docker run --name myfavouriteapp -d -e ENVIRONMENT=DEV samplewebapp

just add “-e” for more variables

  • — restart

You can also add restart policies using “ — restart”

docker run --name myfavouriteapp -d -e ENVIRONMENT=DEV --restart samplewebapp
  • — rm

or you can add “ — rm” to automatically remove the container once it stops

docker run --name myfavouriteapp -d -e ENVIRONMENT=DEV --rm samplewebapp
  • -i & -t (“it)

“-i” stands for interactive, enables to directly interact with the container, however the output is not displayed

“-t” together with i will display the results of your command

docker run -it ubuntu /bin/bash

After running the docker, you see that we are in the root and able to run some commands

We can also select the container we want to open a bash terminal for!

docker exec -it containerid -bash

This opens a bash shell inside the specified container, allowing you to run commands directly

enter “exit” to leave the bash terminal

Map host and container port

What is port mapping in Docker?

The underlying system where Docker is installed is called a Docker engine or a Docker host.

The Docker host automatically starts when we run the Docker desktop on our machine.

When we use the “docker run” command, the container runs in the host.

Run command action by Bob Code https://miro.com/app/board/uXjVKq_PcAE=/?utm_source=showme&utm_campaign=cpa&utm_content=graph

However, for us to interact with the container, we need:

  • A port to “enter” the host
  • This port to be mapped with the container IP

Othwerise we won’t be able to reach the container!

Each container in a docker host is automatically assigned a random IP, but we can also define it explicitely.

So, to access the container, we assign it an IP address followed by its host port number

The IP is internal and only accessible within docker host.

How to map ports in Docker?

docker run — name containername-p hostport:containerport nameofyourrepository:tag

docker run -p 900:80 samplewebapp

This will:

  • Run the docker with host-port 900 and container IP 80
Port Mapping explained by Bob Code https://miro.com/app/board/uXjVKq_PcAE=/?utm_source=showme&utm_campaign=cpa&utm_content=graph

We can now access the container by doing

http://<your_host_ip>:900

Let’s say your host’s IP address is 192.168.1.100, you would access the container at http://192.168.1.100:900.

This will expose a port in the container at address 9000 (http://localhost:9000/)

docker run --name samplewebapp-container -p 9000:80 samplewebapp

if you need to specificy the tag

docker run --name samplewebapp-container -p 9000:80 samplewebapp:1.0.0

In Docker Desktop, navigate to the container tab, you should be able to see your container running

Click on the port (here 9000:80)

You can stop the container process by typing CTRL + C

If your app is not displaying i.e. you only get the nginx home page, it’s because its name needs to be lower case only!

You will have to re-build the image though and change the container name

Otherwise you will get the following error

Error

docker: Error response from daemon: Conflict. The container name “/samplewebapp-container” is already in use by container “id”. You have to remove (or rename) that container to be able to reuse that name.
See ‘docker run — help’.

Solution

  • Remove the container
  • Change your app file name in lower case
  • Clear your browser cache
  • Re-build an image, change the tag accordingly
  • Re-run your container, specifying the right tag

What if we don’t map ports?

Even though the container will be running, we cannot access it!

Let’s see the example below, the container port is 80, but it is not mapped

The port 80 is only exposed

The difference between version 1.0.4 that has a port and version 1.0.3 that doesn’t, we cannot “click” to enter the container as there is no entry point

How not to get into the Docker CLI when doing Docker Run?

What if we just want to run the container and keep our command line busy with other tasks?

just add -d to your command

What it does:

  • Runs container in the background
  • Detaches the command line window
  • We can now run more commands
docker run -d -p 9000:80 --name samplewebapp-container samplewebapp:1.0.4

That keeps the terminal clean

How to check if the container is running?

docker ps -a

Checks all containers

To see all active containers just run

docker ps

These commands also give us the port mapping for each container

Going into the container and running commands

Ok, what if we used the -d command but now we want to actually run some CLI commands into the container

Then just do

docker exec -it my-nginx-container /bin/bash
cat /usr/share/nginx/html/index.html

This command:

  • docker exec: Executes a command inside a running container.
  • -it: “-i” stands for interactive, enables to directly interact with the container, however the output is not displayed, “-t” together with “i” will display the results of your command
  • my-nginx-container: Specifies the name of the container (assuming it's named "my-nginx-container").
  • /bin/bash: Runs a bash shell inside the container.

How to tag Docker containers?

Docker allows you to tag images for easy identification.

You can assign multiple tags to an image:

  • One tag with version number
  • One tag to indicate that it’s the latest

Simply do -t first nameofcontainer:firsttag -t nameofcontainer:secondtag

docker build -t samplewebapp:1.0.4 -t samplewebapp:latest .

Above you can see that samplewebapp same image ID

If the files within the folder in the Dockerfile are updated, then it would create a new image with a different image id

version 1.0.5 is different

How to change the tag?

We can add a new tag to a current image by doing

docker tag nameoftheapp:currenttag nameoftheapp:newtag

docker tag samplewebapp:1.0.4 samplewebapp:latest

Stop, start, restart and kill containers

If your container is running, you must first stop it and then you can remove it: “docker stop dockerid”

docker stop 96

you can start one or more container by just doing: “docker start dockerid”

docker start 195

or restart, which will first stop the running container, and then start it again

docker restart 195

if the container really doesn’t stop then use the command “kill”

docker kill 195

The docker will stop with a status code “137”

Remove containers

To remove a container: “docker rm nameofyourcontainer OR idofyourcontainer” (3 first characters are enough)

docker rm nameofyourcontainer

you can also remove multiple containers by doing docker rm containerid1 containerid2 …

container rm 5a1 195

you can also remove all your containers at once (not recommended for production environments!)

docker rm $(docker ps -a -q)
all containers removed at once!

Remove images

To remove image run “docker rmi repository name OR imageid” (3 first characters are enough)

docker rmi nameofyourimage

you can also remove multiple images at once by doing

docker rmi $(docker images -q)

to avoid getting errors you can do

docker rmi $(docker images -q) --force

What if you have multiple images with the same ID?

Then just use the name of the image and it will untag it

we want to remove the “latest” image
We run this command
And it’s gone!

To remove an image with a specific tag

docker rmi nameofyourimage:tagnumber

Error message

Error response from daemon: conflict: unable to delete id (must be forced) — image is being used by stopped container

Reason

Even though a container is not running, that image is being used in the container

To delete the image, we first need to delete the container

You can also run the force delete (at your own risks!) using -f

docker rmi 123 -f

With these Docker client commands, you’re equipped to efficiently manage Docker containers and images.

In the next section, we’ll explore what makes a Docker image, the Dockerfile

Creating a .Net Dockerfile

What is the Dockerfile?

https://www.geeksforgeeks.org/what-is-dockerfile/

A Dockerfile is a script that contains a series of instructions to assemble a Docker image.

It defines how to build a Docker image, specifying everything from the base image to the commands needed to prepare your application for deployment.

An image in turn is a lightweight, stand-alone, and executable software package that forms the basis to create a container.

In this guide, we will explore how to create a .NET Dockerfile

How to create a dockerfile?

Dockerfiles are simple text files without any extensions. To create a Dockerfile, simply name the file Dockerfile without any extension, and place it in the root directory of your project

.Net Dockerfile process overview

Before diving into creating a .NET Dockerfile, let’s briefly review the typical steps involved in building a .NET application:

  • Restore packages
  • Build the app
  • Publish the app

For a detailed explanation of each step, refer to my blog post on setting up a .NET build pipeline, where I explain the importance of each phase in the development process

Let’s now look at the Docker process

  • Pull the base .Net image
  • Restore
  • Publish (implicitly triggers a build)

Let’s imagine that you have built your solution which includes a .Net .csproj file, you now want to create a container out of it

.Net Dockerfile steps https://miro.com/app/board/uXjVKq-_0_I=/?utm_source=showme&utm_campaign=cpa&utm_content=graph

Let’s see how this would translate into a Dockerfile

Dockerfile

# 1. Base Layer
# Use the official .NET SDK as a build environment
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build

# 2. Build Dependencies Layer
# Set the working directory inside the container
WORKDIR /src

# Copy the solution and project files to the working directory
COPY ["MyApp.sln", "./"]
COPY ["MyApp/MyApp.csproj", "MyApp/"]

# Restore the NuGet packages for the project
RUN dotnet restore "MyApp/MyApp.csproj"

# 3. Application Source Code Layer
# Copy the rest of the application source code to the container
COPY ["MyApp/", "MyApp/"]

# 4. Build Layer
# Build the project and publish the output to the /app directory
WORKDIR /src/MyApp
RUN dotnet publish -c Release -o /app

# 5. Final Layer - Application Startup
# Set the entry point for the application
ENTRYPOINT ["dotnet", "MyApp.dll"]

Creating a .Net Dockerfile manually

If your project doesn’t already contain a Dockerfile, you can create one manually by following these steps

  • Create a new dockerfile (no extension)
  • Go to docker hub and get the .Net image URL

https://hub.docker.com/r/microsoft/dotnet-runtime

  • Pull the base image from a trusted registry like Docker Hub and assign it an alias, such as build or base.

This image will serve as the foundation for your Docker container.

FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build

You should almost always start by pulling a base image from a trusted registry like Docker Hub. Why is that so?

  • Smaller Image Size: Leverage pre-built components like the .NET runtime and SDK from base images, avoiding unnecessary duplication
  • Enhanced Security: Base images are maintained and updated with security patches by trusted sources
  • Improved Efficiency: Base images are often optimised for performance and resource usage
  • Stronger Community Support: Base images have a large community of users, offering support
  • Set the working directory inside the container to /src, where the application’s source code will be stored
WORKDIR /src
  • Copy the solution and project to current directory
COPY ["MyApp.sln", "./"]
COPY ["MyApp/MyApp.csproj", "MyApp/"]
  • Run the dotnet restore command to restore the NuGet packages for the project, ensuring all dependencies are resolved
RUN dotnet restore "MyApp/MyApp.csproj"
  • Copies everything from the host machine’s current directory to the current directory in the container
COPY ["MyApp/", "MyApp/"]

Use .dockerignore to avoid copying unnecessary files

Alternatively, you can copy all files from the current directory on your host machine to the current directory within the container using the COPY . . command

COPY . .
  • Set environment variales in Dockerfile

You can define environment variables in your Dockerfile, such as specifying the application’s port number using the ENV command.

ENV ASPNETCORE_URLS=http://+:80\
  • Publish the build (will automatically build) with flag “Release” in the directory /app/publish
WORKDIR /src/MyApp
RUN dotnet publish -c Release -o /app
  • We make our entry point dotnet, the project being our app

The ENTRYPOINT command specifies the command that will be executed when the container starts. In this case, it runs the application by executing dotnet MyApp.dll

ENTRYPOINT ["dotnet", "MyApp.dll"]

Final file

FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY ./MyApp.sln ./
COPY ./MyApp/MyApp.csproj ./MyApp/

RUN dotnet restore "MyApp/MyApp.csproj"
COPY . .

# Set environment variable in build stage
ENV ASPNETCORE_URLS=http://+:80

RUN dotnet publish -c Release -o /app

FROM mcr.microsoft.com/dotnet/runtime:8.0 AS final
WORKDIR /app
COPY --from=build /app .

ENTRYPOINT ["dotnet", "MyApp.dll"]

Using the Default Dockerfile

Visual Studio can automatically generate a Dockerfile for your .NET project, simplifying the containerization process

How to get the default .Net Dockerfile?

  • Select “Enable container support”
  • Select your OS type
  • Select “Dockerfile”

You can see that there is a Dockerfile and dockerignore out of the box

Default Dockerfile

If you run the docker, you will have a docker dashboard popping-up giving you features to work with your container:

  • Stop, refresh, open a terminal in your container
Container dashboard

You also get access to your environment, labels, ports, volumes…

Environment variables
Ports

Dockerfile Commands Explained

Now that we’ve explored some typical .NET Dockerfiles, let’s dive deeper into the key commands used within them!

FROM

The `FROM` command specifies the base image you want to use for your Docker container.

The syntax is straightforward: FROM + nameofimage (URL)

# Example without a tag
FROM mcr.microsoft.com/dotnet/sdk

We can also specify a tag (7.0)

# Example with a tag
FROM mcr.microsoft.com/dotnet/sdk:7.0

We can give it a name (base)

FROM mcr.microsoft.com/dotnet/sdk:7.0 AS base

Now, we can reuse this image by just calling its name

FROM mcr.microsoft.com/dotnet/sdk:7.0 AS base

## do some other stuff here

FROM base

Then, we can keep adding names

FROM mcr.microsoft.com/dotnet/sdk:7.0 AS base

## do some other stuff here

FROM base as Final

WORKDIR

Sets the working directory within our container

It’s recommended to use relative paths whenever possible for better portability and to explicitly specify the path.

WORKDIR /src

If the WORKDIR doesn’t exist, it will create it

Relative vs absolute paths

This is a relative path

# Current directory
./

# Parent directory
../

It provides a location based on current or parent directory

Example

# a file named data.txt in the current directory
./data.txt

# a file named picture.jpg in the parent directory
../images/picture.jpg

This is an absolute path

/

It provides the exact location of the file or directory

Example

# exact location of a file named report.pdf
/home/user/documents/report.pdf

# exact location of a file on a Windows system
C:\Users\JohnDoe\Desktop\file.txt

COPY

Each COPY command will create a new layer

Takes all files from a source location and copies them into a target location

COPY sourcelocation targetlocation

COPY ./samplewebapp/ /usr/share/nginx/html
  • COPY ./SampleWebApp/

This command navigates to our folder named SampleWebApp and takes all files

./samplewebapp/ Specifies the source directory on your local machine.

  • /usr/share/ngnix/html

Indicates the destination folder in the ngnix server where we paste all the copied files

/usr/share/nginx/html Specifies the target directory inside the container.

To access the files from a named image use — from=nameofyourimage

COPY --from=base /usr/share/nginx/html /src/

We can do the following if WORKDIR is already set

FROM nginx
WORKDIR /usr/share/nginx/html
COPY ./samplewebapp/ .

ARG and ENV

ARG are arguments that can be reused within the Dockerfile

ARGs act just like variables in our Dockerfile

FROM nginx
ARG build_configuration=Release
WORKDIR /usr/share/nginx/html

WORKDIR $buil_configuration
COPY ./samplewebapp/ .

The ARG build_configuration is reused as WORKDIR

ENV are pretty much like ARG, the only difference is when they are used:

  • ENV = runtime
docker run -e "envname=anyvalue" imagename
  • ARG = build time
docker build --build-arg argname=anyvalue

EXPOSE

Although it sounds like EXPOSE is the same than port mapping, it’s actually different

EXPOSE 80

Indicates the port that will be used in the container Host

which is the port that the container will listen to

It is good practice to expose:

  • Port 80 = Http
  • Port 443 = Https

RUN

The most important command in the code.

When used in a CLI context, it will simply run a container

docker run --name myfavouriteapp -p 9000:80 samplewebapp
  • — name = name you want to give to the container
  • -p you must map the port (see port section) leaving this empty won’t enable

The terminal will now be within the container

RUN in the Dockerfile

Now in the Dockerfile, the RUN command, runs any command you give

Let’s see an example within a .net application, where we will use the .Net command

RUN dotnet build "NameOfYourProject.csproj"

Or we can do

RUN dotnet publish "NameOfYourProject.csproj"

These are specific commands for .Net, but any commands will do

Upon running the RUN, a new layer on top of the current image will be created

The layer will have the results of your command.

Each RUN creates a new layer, ensuring that the previous layers are unchanged

ENTRYPOINT

Indicates the first command that will be executed upon entering the container

We could pass CLI commands

ENTRYPOINT ["echo", "Welcome to your container!"]

In the context of .Net, the ENTRYPOINT command is often used after the build to point to the dll file that needs to be executed to start the app

ENTRYPOINT ["dotnet", "dll"]

Example

ENTRYPOINT ["dotnet", "/app/MyApplication.dll"]

Further topics

Docker Hub

Docker Hub is the most widely used container image registry, enabling developers and organizations to store, share, and manage Docker images.

You can perform two primary actions on Docker Hub:

  1. Pull an Image: Download an image from Docker Hub to your local machine.
  2. Push an Image: Upload an image to Docker Hub to share it with others or reuse it later.

https://hub.docker.com/r/microsoft/dotnet

Logging to Docker Hub

Before you can push or pull images, you need to log in to your Docker Hub account. Follow these steps:

  1. Navigate to Your Project Directory: Open your terminal and navigate to the directory containing your Dockerfile.
  2. Log in to Docker Hub: Use the `docker login` command (make sure to enter your username!)
docker login --username yourusername

Enter your Docker Hub username and password if prompted.

  • Navigate to Docker Hub, make sure that you’re logged-in

Now we can:

  • Push images: store and share images
  • Pull images

Push an image

Once you’ve logged in to Docker Hub, you can push your Docker image to the registry. Here’s how:

  • Find your images

Use the `docker images` command to list your available images and note the `IMAGE ID` of the one you want to push

docker images
  • Tag your image properly
docker tag [IMAGE ID] [username/repository:tag]

example

docker tag 255 bobbybob/dotnetdocker:1.0.3

Docker Hub requires the image to be tagged with your Docker Hub username and the repository name.

Otherwise you will get “denied: requested access to the resource is denied”

The format should be: docker tag imageid username/repository:tag.

docker tag 255 bobby12548741/dotnetdocker:1.0.3
  • Push the repository: docker push username/reponame:tag
docker push bobby12548741/dotnetdocker:1.0.3
  • Verify the Push: Visit your Docker Hub account to ensure the image appears in your repository

Pull an image

After pushing an image to Docker Hub, you can pull it to any system or environment. Here’s how:

  • Remove your images locally
  • Copy the image repo + name + tag
  • Paste it into the command: docker pull + username/reponame:tag
docker pull bobby12548741/dotnetdocker:1.0.3

Error

denied: requested access to the resource is denied

  • 1/ make sure that you login with your username!
docker login --username yourusername
  • 2/ Create the repository in your Docker Hub
  • 3/ Tag your resource properly
  • 4/ Make sure that you have docker desktop + docker Hub open in your browser

Volumes in Docker

Containers are inherently stateless, meaning any data created during their runtime is lost once they are stopped.

This can be problematic if you need to persist or share data.

This is where Docker Volumes come into play!

https://docs.docker.com/engine/storage/volumes/

Volumes are the preferred mechanism for persisting data in Docker containers. They allow you to share data between the container and your local machine, or between multiple containers.

To use a volume to your container simply use the

  • “-v”
  • Path of the volume
  • Path in container
docker run -d -v volumepath:containerpath --name nameofthecontainer -p 9000:80 nameoftheimage:tag
  • volumepath: The path on your local machine where the data should be stored.
  • containerpath: The path inside the container where the data should be accessible.

Docker Volume example

docker run -d -v C:\Users\Devoteam\Desktop\docker\backup:/usr/share/nginx/dockerfiles --name webapp -p 9100:80 samplewebapp:1.0.2

Let’s get into our container and add a file

docker exec -it b77 bash

Then go to your container path and create a txt file

Creating myfile.txt in my container
And there it is!

If we stop, remove our container and create a new container, with the same volume, the text file will still be there

myfile.txt is still there

Volumes in Docker deserve their own blog post (especially in terms of databases) so we will leave it here for now ;)

Default Docker File Explained

Running the default Dockerfile commands explained

Run the application that has the default Docker File

Now to the output window, select container tools

You can see the docker commands

The first command

docker build -f "C:\Users\Devoteam\Desktop\docker\dockerfileapp\Dockerfileapp\Dockerfile" --force-rm -t dockerfileapp:dev --target base  --build-arg "BUILD_CONFIGURATION=Debug" --label "com.microsoft.created-by=visual-studio" --label "com.microsoft.visual-studio.project-name=Dockerfileapp" "C:\Users\Devoteam\Desktop\docker\dockerfileapp\Dockerfileapp"
  • Build command
  • -f = file location
  • — force-rm = removes container when stopped
  • -t = tags image
  • — target base =
  • — build-arg = changes the default to “Debug”
  • — label = some labels for documentation
  • “path” = the path of the Dockerfile
docker run -dt -v "C:\Users\Devoteam\vsdbg\vs2017u5:/remote_debugger:rw" -v "C:\Users\Devoteam\AppData\Roaming\Microsoft\UserSecrets:/root/.microsoft/usersecrets:ro" -v "C:\Users\Devoteam\AppData\Roaming\Microsoft\UserSecrets:/home/app/.microsoft/usersecrets:ro" -v "C:\Users\Devoteam\AppData\Roaming\ASP.NET\Https:/root/.aspnet/https:ro" -v "C:\Users\Devoteam\AppData\Roaming\ASP.NET\Https:/home/app/.aspnet/https:ro" -v "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\MSBuild\Sdks\Microsoft.Docker.Sdk\tools\linux-x64\net8.0:/VSTools:ro" -v "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\Common7\IDE\CommonExtensions\Microsoft\HotReload:/HotReloadAgent:ro" -v "C:\Users\Devoteam\Desktop\docker\dockerfileapp\Dockerfileapp:/app:rw" -v "C:\Users\Devoteam\Desktop\docker\dockerfileapp\Dockerfileapp:/src/:rw" -v "C:\Users\Devoteam\.nuget\packages:/.nuget/fallbackpackages:rw" -e "ASPNETCORE_LOGGING__CONSOLE__DISABLECOLORS=true" -e "ASPNETCORE_ENVIRONMENT=Development" -e "DOTNET_USE_POLLING_FILE_WATCHER=1" -e "NUGET_PACKAGES=/.nuget/fallbackpackages" -e "NUGET_FALLBACK_PACKAGES=/.nuget/fallbackpackages" -P --name Dockerfileapp --entrypoint dotnet dockerfileapp:dev --roll-forward Major /VSTools/DistrolessHelper/DistrolessHelper.dll --wait
  • run -dt = run command in detached mode
  • -v = sets the volumes from the Docker container to physical locations
  • -e = sets some environment variables
  • -P = doesn’t specify any port in the published tag, selects any random port allocation

Understanding the Default .NET Dockerfile

FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
USER app
WORKDIR /app
EXPOSE 8080
EXPOSE 8081
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
ARG BUILD_CONFIGURATION=Release
WORKDIR /src
COPY ["Dockerfileapp.csproj", "."]
RUN dotnet restore "./Dockerfileapp.csproj"
COPY . .
WORKDIR "/src/."
RUN dotnet build "./Dockerfileapp.csproj" -c $BUILD_CONFIGURATION -o /app/build
FROM build AS publish
ARG BUILD_CONFIGURATION=Release
RUN dotnet publish "./Dockerfileapp.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "Dockerfileapp.dll"]nginx

This will fetch the image from docker hub and call it “base”

FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
ASP.Net 8 app

So this command will automatically download the image for our container

USER app

USER app sets the default user for the container

WORKDIR /app

This command will set a work directory in this image that will be inside the app folder — if it doesn’t exist it will create it — the current directory is set as this one

EXPOSE 8080
EXPOSE 8081

Exposes the ports of the container (not of the host)

First we get a new image for the build layer

FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
ARG BUILD_CONFIGURATION=Release

We add an argument BUILD_CONFIGURATION with a value (Release) and we set a directory /src (within our build image) and make it as current directory

WORKDIR /src
COPY ["Dockerfileapp.csproj", "."]

In that /src folder, we copy the given csproj and paste into “.” which means our current directory

  • COPY [“what”, “where”]

We then restore packages of the given project

RUN dotnet restore "./Dockerfileapp.csproj"

We build the project with given build configuration and output it in the directory /app/build

COPY . .

Now we are copying all the files (in the current solution directory) into the current directory (in our container)

So all these files

Will go there /src (WORKDIR /src)

WORKDIR "/src/."

We set our current directory again as /src

RUN dotnet build "./Dockerfileapp.csproj" -c $BUILD_CONFIGURATION -o /app/build

Then another command, which creates yet another layer, to build our project:

  • Build what is in this folder “./Dockerfileapp.csproj”
  • Put it here /app/build
  • With configuration $BUILD_CONFIGURATION (=Release)
FROM base AS final

Here we are creating a new image from the base because what happens in between is not relevant

We are setting the directly as /app

WORKDIR /app

Now we actually copy the files from the publish alias image (FROM build as publish)

COPY --from=publish /app/publish .

Within this, we go to the /app/publish folder and copy all the files (“.”) and paste them to our current directory (/app)

ENTRYPOINT ["dotnet", "Dockerfileapp.dll"]

Finally, in order to run the app, we set the entry point to be dotnet and we give it the project name

Thanks for reading!

Further reading

--

Bob Code

All things related to Memes, .Net/C#, Azure, DevOps and Microservices