Containerize a .Net application with Docker: A comprehensive guide
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
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.
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!)
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
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 imagedocker pull
: Fetch an imagedocker run
: Run a container
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
docker pull
: This command fetches an image from a Docker registry, such as Docker Hub, and stores it locallydocker build:
create your image from a Dockerfiledocker run
: create your container based on an imagedocker stop
: stop containerdocker restart
: stop and start containerdocker kill
: abruptly stop your containerdocker rm
: remove your containerdocker rmi
:remove your image
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.
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
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
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
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.
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
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 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 commandmy-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
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)
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
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?
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
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
orbase
.
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
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
You also get access to your environment, labels, ports, volumes…
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:
- Pull an Image: Download an image from Docker Hub to your local machine.
- 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:
- Navigate to Your Project Directory: Open your terminal and navigate to the directory containing your Dockerfile.
- 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!
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
If we stop, remove our container and create a new container, with the same volume, the text file will still be 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
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!