Docker COPY with folder wildcards - docker

Given a file structure like this:
project root
|-- X.sln
|-- src
| |-- Foo
| | |-- Foo.fsproj
| | |-- Foo.fs
| |-- Bar
| |-- Bar.fsproj
| |-- Bar.fs
|-- test
|-- Baz
|-- Baz.fsproj
I'd like to first add all .fsproj files to my Docker image, then run a command, then add the rest of the files. I tried the following, but of course it didn't work:
COPY X.sln .
COPY **/*.fsproj .
RUN dotnet restore
COPY . .
RUN dotnet build
The idea is that after the first two COPY steps, the file tree on the image is like this:
working dir
|-- X.sln
|-- src
| |-- Foo
| | |-- Foo.fsproj
| |-- Bar
| |-- Bar.fsproj
|-- test
|-- Baz
|-- Baz.fsproj
and the rest of the tree is only added in after RUN dotnet restore.
Is there a way to emulate this behavior, preferably without resorting to scripts outside of the dockerfile?

You can use two RUN commands to solve this problem, using the shell commands (find, sed, and xargs).
Follow the steps:
Find all fsproj files, with regex extract the filename without extension and with xargs use this data to create directories with mkdir;
Based on the previous script change the regex to create from-to syntax and use the mv command to move files to newly created folders.
Example:
COPY *.sln ./
COPY */*.fsproj ./
RUN find *.fsproj | sed -e 's/.fsproj//g' | xargs mkdir
RUN find *.fsproj | sed -r -e 's/((.+).fsproj)/.\/\1 .\/\2/g' | xargs -I % sh -c 'mv %'
References:
how to use xargs with sed in search pattern

If you use the dotnet command to manage your solution you can use this piece of code:
Copy the solution and all project files to the WORKDIR
List projects in the solution with dotnet sln list
Iterate the list of projects and move the respective *proj files into newly created directories.
COPY *.sln ./
COPY */*/*.*proj ./
RUN dotnet sln list | \
tail -n +3 | \
xargs -I {} sh -c \
'target="{}"; dir="${target%/*}"; file="${target##*/}"; mkdir -p -- "$dir"; mv -- "$file" "$target"'

One pattern that can be used to achieve what you want without resorting to a script outside the Dockerfile is this:
COPY <project root> .
RUN <command to tar/zip the directory to save a copy inside the container> \
<command the removes all the files you don't want> \
dotnet restore \
<command to unpack tar/zip and restore the files> \
<command to remove the tar/zip> \
dotnet build
This would keep all of your operations inside the container. I've bundled them all in one RUN command to keep all of that activity into a single layer of the build. You can break them out if you need to.
Here's just one example on linux of how to recursively remove all files except the ones you don't want: https://unix.stackexchange.com/a/15701/327086. My assumption, based on your example, is that this won't be a costly operation for you.

Great question and I believe I have found the solution.
Have .dockerignore like this
# Ignore everything except *.fsproj.
**/*.*
!**/*.fsproj
Have your Dockerfile-AA like this (please update the ls)
FROM your-image
USER root
RUN mkdir -p /home/aa
WORKDIR /home/aa
COPY . .
RUN ls
RUN ls src
RUN ls src/p1
RUN ls src/p2
RUN ls src/p3
RUN dotnet restore
Have your docker command like this
sudo docker build --rm -t my-new-img -f Dockerfile-AA .
Run it the first time, it will show only the fsproj file being copied.
Run it again, you cannot see the ls result because it is using cache, great.
Obviously, you cannot have dotnet restore in the same Dockerfile, have another docker file like Dockerfile-BB
FROM my-new-img
COPY . .
RUN dotnet build
So, have your script like this
docker build --rm -t my-new-img -f Dockerfile-AA .
rm -f .dockerignore
docker build --rm -t my-new-img2 -f Dockerfile-BB .
This should work. When building my-new-img, it is going to be blazing fast. You need to try this at least 2 times, because the cache was not created right away in my past experience. This is way better than copying the project file line by line.

Related

.NET package restore in Docker cached separately from build

How does one build a Docker image of a .NET 5/C# app so that the restored NuGet packages are cached properly? By proper caching I mean that when sources (but not project files) are changed, the layer containing restored packages is still taken from cache during docker build.
It is a best practice in Docker to perform package restore before adding the full sources and building the app itself as it makes it possible to cache the restore separately, which significantly speeds up the builds. I know that not only the packages directory, but also the bin and obj directories of individual projects have to be preserved from dotnet restore to dotnet publish --no-restore so that everything works together. I also know that once the cache is busted, all following layers are built anew.
My issue is that I cannot come up with a way to COPY just the *.csproj. If I copy more than just the *.csproj, source changes bust the cache. I could copy them into one place outside the docker build and simply COPY them inside the build, but I want to be able to build the image even outside the pipeline, manually, with a reasonably simple command. (Is it an unreasonable requirement?)
For the web app that consists of multiple projects in a pretty standard folder structure src/*/*.csproj, I came up with this attempt that tries to compensate for too many files being copied into the image (which still busts the cache):
FROM mcr.microsoft.com/dotnet/sdk:5.0 AS build-env
WORKDIR /src
COPY NuGet.Config NuGet.Config
COPY src/ src/
RUN find . -name NuGet.Config -prune -o \! -type d \! -name \*.csproj -exec rm -f '{}' + \
&& find -depth -type d -empty -exec rmdir '{}' \;
RUN dotnet restore src/Company.Product.Component.App/Company.Product.Component.App.csproj
COPY src/ src/
RUN dotnet publish src/Company.Product.Component.App/Company.Product.Component.App.csproj -c Release --no-restore -o /out
FROM mcr.microsoft.com/dotnet/aspnet:5.0 AS run-env
WORKDIR /app
COPY --from=build-env /out .
ENTRYPOINT ["dotnet", "Company.Product.Component.App.dll"]
I also tried splitting the build-env stage into two just after the restore, copying the /root/.nuget/packages and /src to the build stage, but that did not help either.
The first RUN line and the one immediately before should be replaced with something that copies just the *.csproj, but I don't know what that is. The obvious laborious solution is to have a separate COPY line for each *.csproj, but that does not feel right as projects tend to be added and removed so it makes the Dockerfile hard to maintain. I have tried COPY src/*/*.csproj src/ and then fixing the flattened paths, which is a trick that I googled, but it did not work for me as my Docker processes the wildcards just in file names and interprets directory names literally, emitting an error for nonexistent src/* directory. I am using Docker Desktop 3.5.2 (66501), which uses the BuildKit backend to build the images, but I am open to changing the tooling if it helps.
This leaves me clueless about how to satisfy the relatively simple set of my requirements. My options seem exhausted. Have I missed something? Do I have to accept a tradeoff and drop some of my requirements?
The lack of support for wildcards in directory names is likely a missing feature in BuildKit. The issue has already been reported at moby/buildkit GitHub as #1900.
Till the issue is fixed, disable BuildKit if you don't need any of its features. Either
set the environment variable DOCKER_BUILDKIT to zero (0), or
edit the Docker daemon config so that the "buildkit" feature is set to false and restart the daemon.
In Docker Desktop, the config is easily accessible in Settings > Docker Engine. This method of turning off the feature is recommended by the Docker Desktop 3.2.0 release notes where BuildKit was first enabled by default.
Once BuildKit is disabled, replace
COPY src/ src/
RUN find . -name NuGet.Config -prune -o \! -type d \! -name \*.csproj -exec rm -f '{}' + \
&& find -depth -type d -empty -exec rmdir '{}' \;
with
COPY src/*/*.csproj src/
RUN for from in src/*.csproj; do to=$(echo "$from" | sed 's/\/\([^/]*\)\.csproj$/\/\1&/') \
&& mkdir -p "$(dirname "$to")" && mv "$from" "$to"; done
The COPY will succeed without busting the cache and the RUN will fix the paths. It relies on the fact the projects are in the "src" directory, each in a separate directory of the same name as the project file.
This is basically the solution at the bottom of VonC's answer to a related question. The answer also mentions Moby issue #15858, which has an interesting discussion on the topic.
There is a dotnet tool for the paths fixup, too, but I have not tested it.
An alternate solution that does not require disabling BuildKit is to split the original stage in two right after cleaning up the copied files, i.e. just before restore (not after!).
FROM mcr.microsoft.com/dotnet/sdk:5.0 AS projects-env
WORKDIR /src
COPY NuGet.Config NuGet.Config
COPY src/ src/
RUN find . -name NuGet.Config -prune -o \! -type d \! -name \*.csproj -exec rm -f '{}' + \
&& find . -depth -type d -empty -exec rmdir '{}' \;
FROM mcr.microsoft.com/dotnet/sdk:5.0 AS build-env
WORKDIR /src
COPY --from=projects-env /src /src
RUN dotnet restore src/Company.Product.Component.App/Company.Product.Component.App.csproj
COPY src/ src/
RUN dotnet publish src/Company.Product.Component.App/Company.Product.Component.App.csproj -c Release --no-restore -o /out
FROM mcr.microsoft.com/dotnet/aspnet:5.0 AS run-env
WORKDIR /app
COPY --from=build-env /out .
ENTRYPOINT ["dotnet", "Company.Product.Component.App.dll"]
The COPY src/ src/ layer in the sources-env is invalidated by source changes, but cache invalidation works separately for each stage. As the files copied over to the build-env are the same across builds, the COPY --from=projects-env cache is not invalidated, so the RUN dotnet restore layer is taken from the cache, too.
I suspect there are other solutions using the BuildKit mounts (RUN --mount=...), but I haven't tested any.
Here's the alternative way to solve the problem.
First, copy the .sln and .csproj files (depends on the solution folder structure)
COPY *.sln ./
COPY **/*.csproj ./
COPY **/**/**/*.csproj ./
After that run the following script:
RUN dotnet sln list | grep ".csproj" \
| while read -r line; do \
mkdir -p $(dirname $line); \
mv $(basename $line) $(dirname $line); \
done;
The script simply moves .csproj files to the same locations they are in the host filesystem.

Best practice Net Core Api and Docker

I'm trying to find over the net how to manage properly Dockerfile in order to make the best possible image, but unfortunately no good way appeared to me. That's why I ask here.
This is my context :
I'm developping Net Core 3 web API
I'm using template from VS2019
I'm using the original DockerFile with some modifications
Here is my Dockerfile :
#See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging.
FROM mcr.microsoft.com/dotnet/core/aspnet:3.1-buster-slim AS base
RUN apt-get update;apt-get install libfontconfig1 -y
WORKDIR /app
EXPOSE 80
EXPOSE 443
FROM mcr.microsoft.com/dotnet/core/sdk:3.1-buster AS build
WORKDIR /src
COPY ["Src/API/API.csproj", "Src/API/"]
RUN dotnet restore "Src/API/API.csproj"
COPY . .
WORKDIR "/src/Src/API"
RUN dotnet build "API.csproj" -c Release -o /app/build
FROM build AS publish
RUN dotnet publish "API.csproj" -c Release -o /app/publish
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "API.dll"]
There is my solution structure :
.
| .dockerignore
| mySolution.sln
+---Docs
+---Src
| \---API
| | API.csproj
| | API.csproj.user
| | appsettings.Development.json
| | appsettings.json
| | appsettings.Staging.json
| | Dockerfile
| | Dockerfile.original
| | Program.cs
| | Startup.cs
| +---.config
| | dotnet-tools.json
| +---bin
| +---Controllers (source files)
| +---Data (source files)
| +---Database (source files)
| +---Dtos (source files)
| +---Helpers (source files)
| +---Mail (source files)
| +---Migrations (EF source files)
| +---Models (source files)
| +---obj
| +---PDF (source files)
| +---Properties
| | | launchSettings.json
| +---Services (source files)
| \---wwwroot
| +---Templates
| \---uploads
\---Tests
As you can see, if I want to build my image without VS2019, I have to put the Dockerfile to the root directory (where is the .sln file is).
For now, if I use this Dockerfile, Docker will copy all files / directories from Src directory, including bin / obj directories, and wwwroot directory which can contains some files from my upload tests.
If I check in Visual Studio the file structure in my container :
As you can see, I don't need to all files, only my sources in order to build and deploy my app.
How can I upgrade my Dockerfile in order to make the most proper image ?
Some tips:
For security/portability use alpine instead of buster slim in the final image.
At the final image, use "USER NOBODY" to run Dockerfile as non-root.
That will require usage of ports above 1024.
For the building purpose control the current context using '-f' so you can leave Dockerfile inside but use context from solution root and even if you have CI/CD pipelines.
Run your unit tests inside Dockerfile before last stage so if it fails, it would stop.
Think about secrets and that depends where you will run your container because AppConfigs aren't recommended.

Recursively COPY files matching filename and preserve directory structure

Assume I have the following dir structure:
languages
-en-GB
--page1.json
--page2.json
-fr-FR
--page1.json
--page2.json
Now let's assume I want to copy the folder structure, but only page1.json content:
I've tried this:
COPY ["languages/**/*page1.json", "./"]
Which results in the folders being copied, but no files.
What I want to end up with is
languages
-en-GB
--page1.json
-fr-FR
--page1.json
Copied into my image
I am not sure you can use wildcards to produce the filtered result you are looking for.
I believe there are at least two clean and clear ways to achieve this:
Option 1: Copy everything, and cleanup later:
FROM alpine
WORKDIR /languages
COPY languages .
RUN rm -r **/page2.json
Option 2: Add files you don't want into your .dockerignore
# .dockerignore
languages/**/page*.json
!languages/**/page1.json
Option 3: Copy all to a temporary directory, and copy what you need from inside the container using more flexible tools
FROM alpine
WORKDIR /languages
COPY languages /tmp/langs
RUN cd /tmp/langs ; find -name 'page1.json' -exec cp --parents {} /languages \;
CMD ls -lR /languages

Jenkins-Run Docker: COPY failed: stat /var/lib/docker/tmp/docker-builder...: no such file or directory

There are many other questions about this issue, and I've looked at every resource I can find in a google search about this error message, but none of them have worked for me.
I'm using a Jenkins job to automate the build/push of a container. To do so, I'm using the "Execute Shell" build step, and doing all of my Docker configuration within (I've tried using a pipeline, but those just don't work in my team's Jenkins configuration; consider them outside the scope of this question). The job first clones a git repository (myrepo), and then I have a shell script write a number of files that I want to include in the Docker container (e.g. run.sh), as well as a Dockerfile.
The folder structure of my Jenkins workspace is as follows, by the time the build fails:
|-- myrepo/
| |-- myexe # executable file
| |-- testfiles/
| | |-- file1.txt
| | |-- file2.txt
| | |-- ...
|-- run.sh
|-- Dockerfile
I do not have a .dockerignore file.
The Dockerfile, by the time the build fails, looks like this:
FROM centos
USER root
# create directories
RUN mkdir -p /opt/dir1/myexe
# copy shell script
COPY run.sh /usr/bin/runprog
# copy executable
COPY myrepo/myexe /opt/dir1/myexe/ # fails on this line
COPY myrepo/testfiles /opt/dir1/myexe/
# final setup
WORKDIR /opt/dir1/myexe/
CMD ["/opt/dir1/myexe/myexe"]
Now, on Jenkins, I'm executing this all in the "Execute Shell" build step, after having imported the repo earlier. I create run.sh and Dockerfile during this step, using echo and output redirection. The shell script being executed in Jenkins's "Execute Shell" build step looks something like this:
echo '#!/bin/sh
...
' > run.sh
echo 'FROM centos
...
' > Dockerfile
docker system prune -af
docker images # I can confirm from logs that no images remain
docker build -t containername:${DOCKER_IMAGE_TAG} .
But when I run this, the build fails on that docker build command:
Step 5/8 : COPY myrepo/myexe /opt/dir1/myexe
COPY failed: stat /var/lib/docker/tmp/docker-builder190297994/opt/dir1/myexe: no such file or directory
None of the other questions I've seen have solutions that have helped. My files exist locally, which I can see in the Jenkins workspace after the build fails. My files exist locally in the . directory, which I can confirm by putting pwd and ls just before the docker build command. I don't have a .dockerignore file, so Docker shouldn't be ignoring anything. But I must be missing something, as it's still not working no matter what I think to try.
What am I overlooking?
(Docker version is Docker version 18.09.7, build 2d0083d)
Your comment # fails on this line is the problem.
As per the Docker documentation:
Docker treats lines that begin with # as a comment, unless the line is
a valid parser directive. A # marker anywhere else in a line is
treated as an argument.
More info here: https://docs.docker.com/engine/reference/builder/#format

Is there a more elegant way to copy specific files using Docker COPY to the working directory?

Attempting to create a container with microsoft/dotnet:2.1-aspnetcore-runtime. The .net core solution file has multiple projects nested underneath the solution, each with it's own .csproj file. I am attemping to create a more elegant COPY instruction for the sub-projects
The sample available here https://github.com/dotnet/dotnet-docker/tree/master/samples/aspnetapp has a solution file with only one .csproj so creates the Dockerfile thusly:
COPY *.sln .
COPY aspnetapp/*.csproj ./aspnetapp/
RUN dotnet restore
It works this way
COPY my_solution_folder/*.sln .
COPY my_solution_folder/project/*.csproj my_solution_folder/
COPY my_solution_folder/subproject_one/*.csproj subproject_one/
COPY my_solution_folder/subproject_two/*.csproj subproject_two/
COPY my_solution_folder/subproject_three/*.csproj subproject_three/
for a solution folder structure of:
my_solution_folder\my_solution.sln
my_solution_folder\project\my_solution.csproj
my_solution_folder\subproject_one\subproject_one.csproj
my_solution_folder\subproject_two\subproject_two.csproj
my_solution_folder\subproject_three\subproject_three.csproj
but this doesn't (was a random guess)
COPY my_solution_folder/*/*.csproj working_dir_folder/*/
Is there a more elegant solution?
2021: with BuildKit, see ".NET package restore in Docker cached separately from build" from Palec.
2018: Considering that wildcard are not well-supported by COPY (moby issue 15858), you can:
either experiment with adding .dockerignore files in the folder you don't want to copy (while excluding folders you do want): it is cumbersome
or, as shown here, make a tar of all the folders you want
Here is an example, to be adapted in your case:
find .. -name '*.csproj' -o -name 'Finomial.InternalServicesCore.sln' -o -name 'nuget.config' \
| sort | tar cf dotnet-restore.tar -T - 2> /dev/null
With a Dockerfile including:
ADD docker/dotnet-restore.tar ./
The idea is: the archive gets automatically expanded with ADD.
The OP sturmstrike mentions in the comments "Optimising ASP.NET Core apps in Docker - avoiding manually copying csproj files (Part 2)" from Andrew Lock "Sock"
The alternative solution actually uses the wildcard technique I previously dismissed, but with some assumptions about your project structure, a two-stage approach, and a bit of clever bash-work to work around the wildcard limitations.
We take the flat list of csproj files, and move them back to their correct location, nested inside sub-folders of src.
# Copy the main source project files
COPY src/*/*.csproj ./
RUN for file in $(ls *.csproj); do mkdir -p src/${file%.*}/ && mv $file src/${file%.*}/; done
L01nl suggests in the comments an alternative approach that doesn't require compression: "Optimising ASP.NET Core apps in Docker - avoiding manually copying csproj files", from Andrew Lock "Sock".
FROM microsoft/aspnetcore-build:2.0.6-2.1.101 AS builder
WORKDIR /sln
COPY ./*.sln ./NuGet.config ./
# Copy the main source project files
COPY src/*/*.csproj ./
RUN for file in $(ls *.csproj); do mkdir -p src/${file%.*}/ && mv $file src/${file%.*}/; done
# Copy the test project files
COPY test/*/*.csproj ./
RUN for file in $(ls *.csproj); do mkdir -p test/${file%.*}/ && mv $file test/${file%.*}/; done
RUN dotnet restore
# Remainder of build process
This solution is much cleaner than my previous tar-based effort, as it doesn't require any external scripting, just standard docker COPY and RUN commands.
It gets around the wildcard issue by copying across csproj files in the src directory first, moving them to their correct location, and then copying across the test project files.
One other option to consider is using a multi-stage build to prefilter / prep the desired files. This is mentioned on the same moby issue 15858.
For those building on .NET Framework, you can take it a step further and leverage robocopy.
For example:
FROM mcr.microsoft.com/dotnet/framework/sdk:4.8 AS prep
# Gather only artifacts necessary for NuGet restore, retaining directory structure
COPY / /temp/
RUN Invoke-Expression 'robocopy C:/temp C:/nuget /s /ndl /njh /njs *.sln nuget.config *.csproj packages.config'
[...]
# New build stage, independent cache
FROM mcr.microsoft.com/dotnet/framework/sdk:4.8 AS build
# Copy prepped NuGet artifacts, and restore as distinct layer
COPY --from=prep ./nuget ./
RUN nuget restore
# Copy everything else, build, etc
COPY src/ ./src/
RUN msbuild
[...]
The big advantage here is that there are no assumptions made about the structure of your solution. The robocopy '/s' flag will preserve any directory structure for you.
Note the '/ndl /njh /njs' flags are there just to cut down on log noise.
In addition to VonC's answer (which is correct), I am building from a Windows 10 OS and targetting Linux containers. The equivalent to the above answer using Windows and 7z (which I normally have installed anyway) is:
7z a -r -ttar my_project_files.tar .\*.csproj .\*.sln .\*nuget.config
followed by the ADD in the Dockerfile to decompress.
Be aware that after installing 7-zip, you will need to add the installation folder to your environment path to call it in the above fashion.
Looking at the moby issue 15858, you will see the execution of the BASH script to generate the tar file and then the subsequent execution of the Dockerfile using ADD to extract.
Fully automate either with a batch or use the Powershell execution as given in the below example.
Pass PowerShell variables to Docker commands
Qnother solution, maybe a bit slower but all in one
Everything in one file and one command docker build .
I've split my Dockerfile in 2 steps,
First image to tar the *.csproj files
Second image use the tar and setup project
code:
FROM ubuntu:18.04 as tar_files
WORKDIR /tar
COPY . .
RUN find . -name "*.csproj" -print0 | tar -cvf projectfiles.tar --null -T -
FROM mcr.microsoft.com/dotnet/core/sdk:3.1 AS build
WORKDIR /source
# copy sln
COPY *.sln .
# Copy all the csproj files from previous image
COPY --from=tar_files /tar/projectfiles.tar .
RUN tar -xvf projectfiles.tar
RUN rm projectfiles.tar
RUN dotnet restore
# Remainder of build process
I use this script
COPY SolutionName.sln SolutionName.sln
COPY src/*/*.csproj ./
COPY tests/*/*.csproj ./
RUN cat SolutionName.sln \
| grep "\.csproj" \
| awk '{print $4}' \
| sed -e 's/[",]//g' \
| sed 's#\\#/#g' \
| xargs -I {} sh -c 'mkdir -p $(dirname {}) && mv $(basename {}) $(dirname {})/'
RUN dotnet restore "/src/Service/Service.csproj"
COPY ./src ./src
COPY ./tests ./tests
RUN dotnet build "/src/Service/Service.csproj" -c Release -o /app/build
Copy solution file
Copy project files
(optional) Copy test project files
Make linux magic (scan sln-file for projects and restore directory structure)
Restore packages for service project
Copy sources
(optional) Copy test sources
Build service project
This is working for all Linux containers

Resources