.NET Tools

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!

image description

Discover more