Understanding multi stage DockerFile builds - docker

I am new to Docker. I have the following directory structure for my project
docker-compose.yml
|-services
|-orders
|-DockerFile
I am using standard ASP.Net Core DockerFile that has the following content:
FROM mcr.microsoft.com/dotnet/core/aspnet:3.1 AS base
WORKDIR /app
EXPOSE 80
FROM mcr.microsoft.com/dotnet/core/sdk:3.1 AS build
WORKDIR /src
COPY ["Services/Orders/Orders.csproj", "Services/Orders/"]
RUN dotnet restore "Services/Orders/Orders.csproj"
COPY . .
WORKDIR "/src/Services/Orders"
RUN dotnet build "Orders.csproj" -c Release -o /app/build
FROM build AS publish
RUN dotnet publish "Orders.csproj" -c Release -o /app/publish
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "Orders.dll"]
My docker-compose.yml file has
# Please refer https://aka.ms/HTTPSinContainer on how to setup an https developer certificate for your ASP .NET Core service.
version: "3.4"
services:
orders-api:
image: orders-api
build:
context: .
dockerfile: Services/Orders/Dockerfile
ports:
- "2222:80"
I have some confusion with these two files
Question 1: What is the use of WORKDIR /app on line number 2?
My understanding is that we are using a base image that we can extend so
when we import the base image in line number 1 and then set the
WORKDIR and port in line number 2 and 3, will they be used by following commands
that use this image?
Question 2: Why are we setting WORKDIR /src for the SDK image and not WORKDIR /app?
Question 3: Are paths in copy commands relevant to Dockerfile or Docker-compose.yml file?
In the line COPY ["Services/Orders/Orders.csproj", "Services/Orders/"], the path that we are specifying seems to be relevant to the docker-compose.yml file and not the DockerFile which is nested down further in folders. Does this mean that paths in Dockerfile need to be relevant to docker-compose.yml? I am asking this because I am thinking that if I run the docker build command using this Dockerfile then I will get an error since the path in the copy command will not be valid.

For anyone coming to this thread later and facing a similar issue, I am going to share what I have learned so far based on Leo’s answer and other sources.
Docker has a feature called multi-stage builds. This allows us to create multiple layers and use them.
ASP.Net Core app can be built using different images. The SDK image is larger in size but gives us additional tools to build and debug our code in a development environment.
In production, however, we do not need the SDK. We only want to leverage the lightweight run time in PROD. We can leverage the multi-stage builds feature of Docker to create our final container and keep it lightweight.
Coming to the Dockerfile…
Each section that starts with the “From” keyword is defining a layer. In my original Dockerfile, you can see I am creating 4 layers base, build, publish and final. We can name a layer and then use that name to create a new layer based on the first one.
FROM mcr.microsoft.com/dotnet/core/aspnet:3.1 AS base
WORKDIR /app
EXPOSE 80
The above code will create a layer called “base” based on the aspnet:3.1 image which contains the lightweight runtime and will be used to host our final application.
We then set the working directory to /app folder so that when we use this layer later, our commands such as COPY will run in this folder.
Next, we just expose port 80 which is the default port for the web-server.
FROM mcr.microsoft.com/dotnet/core/sdk:3.1 AS build
WORKDIR /src
COPY ["Services/Orders/Orders.csproj", "Services/Orders/"]
RUN dotnet restore "Services/Orders/Orders.csproj"
COPY . .
WORKDIR "/src/Services/Orders"
RUN dotnet build "Orders.csproj" -c Release -o /app/build
Next, we download the SDK image which we will use to build the app and call this layer "build".
We set the working directory that will be used by the following "COPY" commands.
The Copy command copies files from local file system to the specified location in the container. So essentially I am copying my Orders.csproj file into the container and then running "dotnet restore" to restore all Nuget packages that are required for this project.
Copying only the .csproj or .sln file and then restoring NuGet packages without copying the entire code is more efficient as it utilizes caching as mentioned here and is a widely adopted practice which I didn’t know and was wondering why can’t we simply copy everything and just run "dotnet restore" command?
Once we have restored NuGet packages we can copy all the files and then run the "dotnet build" command to build our project.
FROM build AS publish
RUN dotnet publish "Orders.csproj" -c Release -o /app/publish
Nest, we create a new layer called "publish" based on our previous layer "build" and simply publish our project with release configuration in the "app/publish" directory.
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "Orders.dll"]
The final layer uses "base" (our lightweight runtime image). Copies files from publish layer’s /app/publish location to the set working directory and then set’s the entry point of the application.
So we are using 2 separate images. SDK to build our app, and aspnet image to host our app in the container but the important thing to note here is that the final container that will be generated will only contain the aspnet runtime and will be smaller in size.
All these things are documented and can be searched but I struggled because the information was scattered. Hopefully, it will save some time to anyone that is new to ASP.Net Core and Docker in the future.

Question 1: What is the use of WORKDIR /app on line number 2?
This defines your current working directory. Following commands will by default be run from that location and relative paths will start there.
Example with the following file structure:
/app/
- README.md
- folder1/
- some.txt
WORKDIR /app
RUN ls
will print
folder1
README.md
and
WORKDIR /app/folder1
RUN ls
will print
some.txt
Question 2: Why are we setting WORKDIR /src for the SDK image and not WORKDIR /app?
See the answer to Question 1. The following COPY and dotnet restore command are executed in the /src source directory. (The build is done from the /src directory and the created artifact in /app/publish is later copied to the /app directory in the last stage, to be executed from that /app directory.)
Question 3: Are paths in copy commands relevant to Dockerfile or Docker-compose.yml file?
Copy takes two paths. The first one references the source (a file or folder from the context the docker image is build from) and the second one references the destination (a file order folder in your resulting docker image.) Hence these paths are usually only specific to the Dockerfile and independent from your docker-compose project.
However in your case the context of your docker image build is defined in the docker-compose.yml here:
build:
context: .
dockerfile: Services/Orders/Dockerfile
and therefore the context of your docker image image build seems to be the directory where your docker-compose.yml is located. You could build the same image though if you just run docker build -f Services/Orders/Dockerfile . in that folder. (It is not docker-compose specific)
Therefore you should find Orders.csproj in ./Services/Orders/ starting from the directory your docker-compose.yml is located in. This file is than copied to /src/Services/Orders/Orders.csproj in your second build stage. (The /src can be commited in the COPY statement as it is a relative path starting from your current working directory, which is defined in the line above. - See Question 1)

The reason we use "multi-stage" builds and use several images and copy files to continue instead of just sequentially carrying out steps is mainly due to trying to keep the image size small. Although disk space may be adequate, the following factors can also be relevant:
Build/deploy times. Larger images mean longer continuous integration, the more time will be spent waiting for these operations to complete.
Start-up time. When running your application in production the longer the download takes, the longer it will be before the new container can be up and running.
Therefore using a multi-stage approach as below we are able to omit the sdk which is much bigger and is only needed for building not running the application:
Notice that we use ENTRYPOINT in the second image as the "dotnet run" is only available in the sdk so we make a minor adjustment to just directly run the .dll to get the same outcome.

Related

How to start multiple microservices after docker build finished for .net projects?

I have a .net solution which has 2 projects, and each project is a microservice (a server). I have a dockerfile which first installs all the dependencies which are used by both projects. Then I publish the solution:
FROM mcr.microsoft.com/dotnet/sdk:5.0 AS build
WORKDIR /app
# Copy csproj and restore as distinct layers
COPY *.sln .
COPY Server/*.csproj ./Server/
COPY JobRunner/*.csproj ./JobRunner/
RUN dotnet restore ./MySolution.sln
# Copy everything else and build
COPY . ./
RUN dotnet publish -c Release -o out
# Build runtime image
FROM mcr.microsoft.com/dotnet/aspnet:5.0
WORKDIR /app
COPY --from=build /app/out .
ENTRYPOINT ["dotnet", "Server.dll"]
When the solution is published, 2 executables are available: Server.dll and JobRunner.dll. However, I can only start only one of them in Dockerfile.
This seems to be wasteful because restoring the solution is a common step for both Server and JobRunner project. In addition this line RUN dotnet publish -c Release -o out produces both an executable for Server and JobRunner. I could write a separate Dockerfile for each project but this seems redundant as 99% of the build steps for each project is identical.
Is there a way to somehow start 2 executables from a single file without using a script (I don't want that both services will be started in a single container)? The closest I've found is the --target option in docker build but it probably won't work because I'd need multiple entrypoints.
In your Dockerfile, change ENTRYPOINT to CMD in the very last line.
Once you do this, you can override the command by just providing an alternate command after the image name in the docker run command:
docker run ... my-image \
dotnet JobRunner.dll
(In principle you can do this without changing the Dockerfile, but the docker run construction is awkward, and there's no particular benefit to using ENTRYPOINT here. If you're using Docker Compose, you can override either entrypoint: or command: on a container-by-container basis.)

Why we need multiple copy instruction in DockerFile for Asp.net core [duplicate]

This question already has an answer here:
How to use dotnet restore properly in Dockerfile
(1 answer)
Closed 2 years ago.
Currently I am learning docker. I was going through the official documentation to create Docker images for ASP.NET Core that shows a DockerFile with following content
FROM mcr.microsoft.com/dotnet/core/sdk:2.2 AS build-env
WORKDIR /app
# Copy csproj and restore as distinct layers
COPY *.csproj ./
RUN dotnet restore
# Copy everything else and build
COPY . ./
RUN dotnet publish -c Release -o out
# Build runtime image
FROM mcr.microsoft.com/dotnet/core/aspnet:2.2
WORKDIR /app
COPY --from=build-env /app/out .
ENTRYPOINT ["dotnet", "aspnetapp.dll"]
I successfully created the image and container eventually.
Here, in the DockerFile we have two COPY instruction.
first one, copy only the *.csproj files from that directory and paste it to /app directory inside docker image.
second one, copy all the other files files and paste it to /app directory inside docker image.
As far as understand, this tow COPY instruction in doing the same thing. so, instead of two different copy instruction, we can write one and it works fine. Like this -
COPY . ./
RUN dotnet restore
RUN dotnet publish -c Release -o out
so, why we are using two different instruction to copy files to docker image? Does it perform well with two copy instruction?
In the first COPY instruction, there is a comment #Copy csproj and restore as distinct layers. What it actually means or does?
The reason is layers. In a Dockerfile, each command creates a new container layer. To speed the building of containers, Dockers caches these layers, where nothing has changed, meaning that command doesn't have to be run again; the cached layer is simply used. However, once something has changed, every single subsequent command in the Dockerfile must be run again, because that layer and all layers built on it are now invalid.
The reason for the two copy lines is that that pulling all the NuGet packages can take a while, and the only thing necessary for this to happen are the project files. Those project files aren't likely to change very often, so the NuGet restore layer can be cached. If you copied all the files, then any change to any file would invalidate the restore layer, meaning it would basically never be cached.

.NetCore microservice CI pipeline Azure DevOps Build error # docker image

I am very much new on docker technology, I am getting the build error while creating the .Net Core 3.1 on Azure DevOps CI pipeline on Docker image tasks:
Step 7/17 : COPY ["API2/API2.csproj", "API2/"] COPY failed: CreateFile
\?\C:\ProgramData\docker\tmp\docker-builder021493529\API2\API2.csproj:
The system cannot find the path specified.
My default docker file is
FROM mcr.microsoft.com/dotnet/core/aspnet:3.1-nanoserver-1809 AS base
WORKDIR /app
EXPOSE 80
EXPOSE 443
FROM mcr.microsoft.com/dotnet/core/sdk:3.1-nanoserver-1809 AS build
WORKDIR /src
COPY ["API1/API1.csproj", "API1/"]
RUN dotnet restore "API1/API1.csproj"
COPY . .
WORKDIR "/src/API1"
RUN dotnet build "API1.csproj" -c Release -o /app/build
FROM build AS publish
RUN dotnet publish "API1.csproj" -c Release -o /app/publish
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "API1.dll"]
Please do let me know from where I am doing mistake.
Here are the docker tasks:
Here are the folder and files structure on azure DevOps as well:
COPY ["API1/API1.csproj", "API1/"]
Based on the error message, this should the line that caused the error message.
Step1:
Please ensure you did not configure the .dockerignore file to exclude this file: API1/API1.csproj, which must exists in the directory where you run your build from.
Step2:
After above confirmed, now we can consider the error is caused about the server could not find the csproj file correctly by following the context and the path you provided.
According to the your original definition: API1/API1.csproj, I guess the actual path of API1.csproj in your repository should be src/API1/API1.csproj, right?
If yes, here has 2 method you can try:
1). Change the COPY definition as:
COPY ["API1.csproj", "API1/"]
Updated:
When you apply this method, you may succeed to COPY, but failed with Program does not contain a static 'Main' method suitable for an entry point *****.
Here it means that the COPY . . does not copy the files correctly.
At this time, please also change the COPY . . to COPY . API1/. This will add folder to dest path.
2). Another way is you could specify API1 to Build context in task:
Below is what I am using, and I do not need make any changes into my dockerfile:
you can input $(Build.Repository.LocalPath) by replacing hard code the context:
Updated:
In Docker 2.*, you can also leave Build context to **:
You can refer to my previous answer on such question: #1.
Based on my opinions, I am not recommend the first method I mentioned above, because it let your dockerfile different with the one which you can run successfully in Visual studio.

Where is this dockerfile putting files in the container?

I'm using Docker to build an ASP site, and I'm confused about where my files are going. Here's my dockerfile (the application is called AspCore)
FROM microsoft/dotnet:2.2-sdk as build
ARG BUILDCONFIG=RELEASE
ARG VERSION=1.0.0
COPY AspCore.csproj /build/
RUN dotnet restore ./build/AspCore.csproj
COPY . ./build/
WORKDIR /build/
RUN dotnet publish ./AspCore.csproj -c $BUILDCONFIG -o out /p:Version=$VERSION
FROM microsoft/dotnet:2.2-aspnetcore-runtime
WORKDIR /app
COPY --from=build /build/out .
ENTRYPOINT ["dotnet", "AspCore.dll"]
This works correctly and I can access the site when I run the image, but I don't quite understand how this is working. When I open a shell to my container, I don't see a build directory either in the app directory or in the base directory. I also can't find the AspCore.csproj file.
Isn't the dockerfile copying AspCore.csproj into the build directory, so shouldn't there be a build directory with a bunch of files in it on my container? What am I misunderstanding?
That's just because you're using 2 stage on your Dockerfile to build your image.
Stage 1: (BUILD) Based on microsoft/dotnet:2.2-sdk as build image to build your source code into dll files
Stage 2: (SERVE) Based on microsoft/dotnet:2.2-aspnetcore-runtime to create a runtime for your dlls, in this stage you've already copy files from previous stage into a folder called app by this line COPY --from=build /build/out ..
After stage 2 copied files from stage1, nothing else you can see from stage1 but copied files, that's why when you start your container you didn't see /build folder
This pattern is good for production build when you want to minimize your image, because actually we don't need sdk in production environment, we just need runtime for the compiled code.
Hope that clear enough, for more information, you can take a look at this article

Why can i not find MSBuild in multi solution asp.net-core application within a docker container

I am trying to contain my asp.net-core application into a docker container. As I use the Microsoft-secret-store for saving credentials, I need to run a dotnet user-secrets command withing my container. The application needs to read these credentials when starting, so I have to run the command prior to starting my application. When trying to do that in my Dockerfile I get the error:
---> Running in 90f974a06d83
Could not find a MSBuild project file in '/app'. Specify which project to use with the --project option.
I tried building my application first and then building a container with the already build dll, but that gave me the same error. I also tried connecting to the container with ENTRYPOINT ["/bin/bash"] and then looking around in the container. It seems that the /app folder that gets created does not have the .csproj files included. Im not sure if that could be an error.
My Dockerfile
FROM mcr.microsoft.com/dotnet/core/sdk:2.2 AS base
WORKDIR /app
EXPOSE 80
EXPOSE 443
FROM mcr.microsoft.com/dotnet/core/sdk:2.2-stretch AS build
WORKDIR /src
COPY ["Joinme.Facade/Joinme.Facade.csproj", "Joinme.Facade/"]
COPY ["Joinme.Domain/Joinme.Domain.csproj", "Joinme.Domain/"]
COPY ["Joinme.Business/Joinme.Business.csproj", "Joinme.Business/"]
COPY ["Joinme.Persistence/Joinme.Persistence.csproj", "Joinme.Persistence/"]
COPY ["Joinme.Integration/Joinme.Integration.csproj", "Joinme.Integration/"]
RUN dotnet restore "Joinme.Facade/Joinme.Facade.csproj"
COPY . .
WORKDIR "/src/Joinme.Facade"
RUN dotnet build "Joinme.Facade.csproj" -c Release -o /app
FROM build AS publish
RUN dotnet publish "Joinme.Facade.csproj" -c Release -o /app
FROM base AS final
WORKDIR /app
COPY --from=publish /app .
RUN dotnet user-secrets set "jwt:secret" "some_password"
ENTRYPOINT ["dotnet", "Joinme.Facade.dll"]
My expected results are that the secret gets set, so I can start the container without it crashing.
Plain and simple: the operation is failing because at this stage, there is no *.csproj file(s), which the user-secrets command requires. However, this is not what you should be doing anyways for a few reasons:
User secrets are not for production. You can just as easily, or in fact more easily, set an environment variable here instead, which doesn't require dotnet or the SDK.
ENV jwt:secret some_password
You should not actually be storing secrets in your Dockerfile, as that goes into your source control, and is exposed as plain text. Use Docker secrets, or an external provider like Azure Key Vault.
You don't want to build your final image based on the SDK, anyways. That's going to make your container image huge, which means both longer transfer times to/from the container registry and higher storage/bandwidth costs. Your final image should be based on the runtime, or even something like alpine, if you publish self-contained (i.e. keep it as small as possible).

Resources