4
\$\begingroup\$

I use a single dockerfile to build and run the following images:

Production application image:

docker build -t myapplication.program -f src/MyApplication.Program/dockerfile  --target runner --build-arg version=1.0.19031.1 .

docker run --rm -it -e ASPNETCORE_ENVIRONMENT=local -p 5050:5050 myapplication.program:latest

Unit test and code coverage image:

docker build -t myapplication.tests -f src/MyApplication.Program/dockerfile --target tester .

docker run --rm -it -v $PWD/codecoveragereports:/app/test/MyApplication.UnitTests/codecoveragereports myapplication.tests:latest

Dockerfile:

# Define the base image with .NET Core SDK to enable restoring, building & publishing the code.
FROM microsoft/dotnet:2.1-sdk AS builder
# Set working directory in the container to /app. All further actions to affect this directory.
WORKDIR /app
# Copy csproj files from the source (period indicating the root solution directory) to our image (period indicating working directory).
COPY ./src/MyApplication.Program/MyApplication.Program.csproj ./src/MyApplication.Program/MyApplication.Program.csproj
COPY ./src/MyApplication.Interfaces/MyApplication.Interfaces.csproj ./src/MyApplication.Interfaces/MyApplication.Interfaces.csproj
COPY ./src/MyApplication.SignalR/MyApplication.SignalR.csproj ./src/MyApplication.SignalR/MyApplication.SignalR.csproj
COPY ./src/MyApplication.Protocol/MyApplication.Protocol.csproj ./src/MyApplication.Protocol/MyApplication.Protocol.csproj
COPY ./src/MyApplication.Domain/MyApplication.Domain.csproj ./src/MyApplication.Domain/MyApplication.Domain.csproj
COPY ./src/MyApplication.Infrastructure/MyApplication.Infrastructure.csproj ./src/MyApplication.Infrastructure/MyApplication.Infrastructure.csproj
COPY ./src/MyApplication.Services/MyApplication.Services.csproj ./src/MyApplication.Services/MyApplication.Services.csproj
COPY ./src/MyApplication.Actors/MyApplication.Actors.csproj ./src/MyApplication.Actors/MyApplication.Actors.csproj
# Copy nuget.config to root working directory - dotnet restore will use this.
COPY nuget.config ./
# Set working directory to starting project folder.
WORKDIR ./src/MyApplication.Program/
# Run dotnet package restore on all projects.
RUN dotnet restore
# Reset working directory back to /app
WORKDIR /app
# Copy all source code to image.
COPY ./src ./src
# Set working directory to starting project folder.
WORKDIR ./src/MyApplication.Program/
# Build the main project
RUN dotnet build MyApplication.Program.csproj -c Release

FROM builder as tester
# Set working directory in the container to /app. All further actions to affect this directory.
WORKDIR /app
# Copy csproj files from the source (period indicating the root solution directory) to our image (period indicating working directory).
COPY ./test/MyApplication.UnitTests/MyApplication.UnitTests.csproj ./test/MyApplication.UnitTests/MyApplication.UnitTests.csproj
# Set working directory to starting project folder.
WORKDIR ./test/MyApplication.UnitTests/
# Run dotnet package restore on all projects.
RUN dotnet restore
# Reset working directory back to /app
WORKDIR /app
# Copy all source code to image.
COPY ./test ./test
# Set working directory to starting project folder.
WORKDIR ./test/MyApplication.UnitTests/
# Build the main project
RUN dotnet test /p:CollectCoverage=true /p:CoverletOutputFormat=cobertura
# Entry point command to generate the reports
ENTRYPOINT ["dotnet", "reportgenerator", "-reports:coverage.cobertura.xml", "-targetdir:codecoveragereports", "-reportTypes:htmlInline"]

FROM builder as publisher
# Reset working directory back to /app
WORKDIR /app
# Set working directory to starting project folder.
WORKDIR ./src/MyApplication.Program/
# Expect an argument for the artefact version number.
ARG version
# Dotnet publish project.
RUN dotnet publish MyApplication.Program.csproj -c Release -o publish /p:Version=$version

# Define the base image with just the (lightweight) .NET Core runtime to host and run MyApplication.
FROM microsoft/dotnet:2.1-aspnetcore-runtime AS runner
# Set working directory in the container to /app. All further actions to affect this directory.
WORKDIR /app
# Copy published output from the builder image.
COPY --from=publisher /app/src/MyApplication.Program/publish/ .
# Listen on this port at runtime.
EXPOSE 5050
# Set application entry point
ENTRYPOINT ["dotnet", "MyApplication.Program.dll"]

Please code review my dockerfile.

\$\endgroup\$

1 Answer 1

4
\$\begingroup\$

It's been a few years since you asked this question. I'm honestly not sure how much of the docker suggestions I make will have been relevant when you asked your question and I expect the application you are building to have evolved since then, but here's some suggestions nonetheless:

Comment style

You are commenting basically every step of the process. This is probably overkill. Most of these comments are also obviously not useful for readers, since they just reformulate what the code does instead of giving context for why it is the way it is. Basically ALL comments that are not on FROM statements are affected by this.

You can safely drop most of these comments in favor of explaining why you're doing things the way you are. I would suggest explicitly clearing up that the long string of COPY commands for the restore is a caching optimization that speeds up builds when no restore is going to be required. All the other comments seem to add little value for me.

dotnet can traverse directories

You're making use of WORKDIR to do the job of directory traversal for dotnet. That is unnecessary, since all dotnet commands have a way to alter their working directory automatically.

COPY nuget.config ./

RUN dotnet restore src/MyApplication.Program
COPY src src
RUN dotnet build src/MyApplication.Program/MyApplication.Program.csproj -c Release

docker is not a scripting language

I dislike that your tester frontloads its testing steps into the docker build process. Actually, thinking about it, I dislike that your docker build does all that RUN dotnet stuff. As it is you're abusing the dockerfile to be a scripting language.

Really, what you are doing here is just formalizing your build steps into a dockerfile. There is some good reasons to do that, but I would suggest that this is not what docker should be used for.

What you have here instead is a CI definition in disguise. You have four stages: build, test, publish and deploy. Each of these steps does not necessarily need to run on a developer machine if you want to release your application. In fact I posit that it shouldn't.

Your testing image is a glorified archive for coverage reports generated at docker build time. That's not really a useful image. Use a CI tool for your CI needs and use docker to supplement that CI tool with reproducible, software-defined environments.

\$\endgroup\$
1
  • 1
    \$\begingroup\$ Even for commands that do need a specific working directory, WORKDIR is usually the wrong approach, compared to cd $dir && at the start of the RUN command. BTW, fully agree that abusing Docker for its shell is weird and inefficient. \$\endgroup\$ Commented May 27, 2023 at 14:55

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.