.NET Tools
Essential productivity kit for .NET and game developers
How Docker Fast Mode Works in Rider
JetBrains Rider’s Docker fast mode helps speed up the feedback loop when building, debugging, and running containerized applications. With fast mode, Rider builds your application locally and then mounts it within a Docker container for a production-like experience.
In this post, we’ll look at what Rider is doing under the hood to make fast mode work, and how it launches your containers when you start a run configuration.
Faster feedback loops
Some developers prefer to run their container applications as regular .NET projects during development. A typical scenario would be to run their backing services (e.g. database, queue or something similar) with Docker Compose, open local ports, and then run the .NET project on their workstation.
The advantage of this approach is that it lets you apply changes to the application much faster. You don’t need to rebuild the application container image and wait for it. In other words, you get a much quicker response after making your changes.
The drawback is that it is inconvenient to have different Docker Compose files for different environments. For example, having the same configuration for all environments can lead to inadvertently opening development ports in production, exposing your application to malicious attackers. Keeping track of differences between environments can be a hassle. Ugh, so much to remember!
And this is where our Docker fast mode can help. We use the same Docker Compose and Dockerfiles that you use for all environments, but apply some overrides to start the container quicker during development. Let’s have a look.
Under the hood
JetBrains Rider’s Docker fast mode approach is based on Docker’s multi-stage build feature. In short, multi-stage builds create environment checkpoints that can be reused between rebuilds. Let’s look at an example of a common Dockerfile for an ASP.NET Core application:
FROM mcr.microsoft.com/dotnet/aspnet:7.0 AS base WORKDIR /app EXPOSE 80 EXPOSE 443 FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build WORKDIR /src COPY ["WebApp/WebApp.csproj", "WebApp/"] RUN dotnet restore "WebApp/WebApp.csproj" COPY . . WORKDIR "/src/WebApp" RUN dotnet build "WebApp.csproj" -c Release -o /app/build FROM build AS publish RUN dotnet publish "WebApp.csproj" -c Release -o /app/publish /p:UseAppHost=false FROM base AS final WORKDIR /app COPY --from=publish /app/publish . ENTRYPOINT ["dotnet", "WebApp.dll"]
As you can see, there are several FROM instructions, also known as stages, with different names of base
, build
, publish
, and final
. Each stage can copy artifacts from other stages (for example, COPY --from=publish /app/publish .
).
Now, why would you use different stages?
The benefit is that you can use different base images for different stages. You can use the mcr.microsoft.com/dotnet/sdk
image (almost 800 MB) to build the application and then use the mcr.microsoft.com/dotnet/aspnet
image (about 200 MB) to create the final image that runs your application – copying the prepared artifact from the build
stage. Therefore, the final image size will be much smaller than if you had used the sdk
base image.
This is the multi-stage build technique, and it is quite common nowadays.
For Docker fast mode, we use it differently. You can specify the --target
option to stop the build at a particular stage.
docker build -t web-app --target base .
Only the base
stage will be built here.
Docker heavily uses caches when building the image; if there are no changes, Docker won’t do anything. And if you look at the first stage, there’s nothing to change.
FROM mcr.microsoft.com/dotnet/aspnet:7.0 AS base WORKDIR /app EXPOSE 80 EXPOSE 443
Thus, if we build this stage once, the following builds will be very fast.
But there is still one small problem… We don’t have our application inside the image. And we can’t build it inside because we’re using the base image mcr.microsoft.com/dotnet/aspnet
rather than mcr.microsoft.com/dotnet/sdk
, which comes with the full toolchain
The good news is that we can build the application on the host machine and then simply attach the folder to the container. Here you can see this volume attached to the container (under Volumes) from the Services tool window.
Finally, Rider modifies the entrypoint to run your application when the container starts.
You can check the entrypoint (["dotnet", "/app/bin/Debug/net7.0/WebApplication1.dll" ]
) on the container’s Inspection tab.
That’s pretty much it. Rider also sets some useful environment variables in fast mode, and exports and attaches a certificate to the container.
The final command line created in the background by Rider will look similar to this:
docker build -f .\WebApplication1\Dockerfile --target base -t webapplication1:dev . && docker run --name webapplication1 -v {Path to the WebApplication1 folder}:/app --entrypoint dotnet webapplication1:dev /app/bin/Debug/net7.0/WebApplication1.dll
Customizing Docker fast mode
If you need to include specific tools or configurations in your development container image without affecting your production image, you’re in luck! It is possible to manually specify the required stage using the MSBuild DockerfileFastModeStage
property in the project file.
Let’s look at an example. Imagine you want to include dotnet-counters to monitor some valuable metrics. No problem! You can add a new dev
stage and install dotnet-counters
.
FROM mcr.microsoft.com/dotnet/aspnet:7.0 AS base WORKDIR /app EXPOSE 80 EXPOSE 443 FROM base as dev RUN apt-get update && apt-get -y install curl RUN mkdir /tools && cd /tools && curl -JLO https://aka.ms/dotnet-counters/linux-x64 && chmod +x ./dotnet-counters FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build WORKDIR /src COPY ["WebApplication1/WebApplication1.csproj", "WebApplication1/"] RUN dotnet restore "WebApplication1/WebApplication1.csproj" COPY . . WORKDIR "/src/WebApplication1" RUN dotnet build "WebApplication1.csproj" -c Release -o /app/build FROM build AS publish RUN dotnet publish "WebApplication1.csproj" -c Release -o /app/publish /p:UseAppHost=false FROM base AS final WORKDIR /app COPY --from=publish /app/publish . ENTRYPOINT ["dotnet", "WebApplication1.dll"]
Next, add the DockerfileFastModeStage
property to your project file:
<DockerfileFastModeStage>dev</DockerfileFastModeStage>
Rider will now use this dev
stage to create a fast mode image, so you can run dotnet-counters
inside the container. And since no other stages depend on this dev
stage, the final image won’t contain these changes.
Conclusion
In this post, we discussed Docker fast mode and how it helps speed up your development feedback loop when debugging and running your application in a container. We’ve also seen how Rider modifies the Docker deployment to support Docker fast mode and can provide a faster way to run containers.
Download Rider and give it a try, and let us know how you’re using Docker fast mode!