Each week I'll look at one Dockerfile in detail, showing you what it does and how it works. This is #21 in the series, where I start looking at container-first solution design, and using Docker to modernize existing applications.
Container-First Solution Design
Chapter 5 is all about putting containers at the centre of your thinking when you design new applications or new features. It's not about microservices - it works just as well for new projects and for extending and improving your existing monoliths.
There are two parts to thinking container-first:
using containers as the unit of deployment for application features, so you add new capabilities in discrete, independent components.
I walk through the whole approach in this chapter, using the venerable Nerd Dinner as my sample app.
Nerd Dinner in Chapter 5
Chapter 5 stars with Nerd Dinner already packaged to run in Docker - I covered that in the Dockerfiles from Chapter 3. The web app runs in one container, it connects to SQL Server running in another container, and loads the homepage from a third container.
By the end of the chapter, key features are broken out of the monolith and new functionality is added - all in components running in separate containers. The major change to the original web application is to extract the "save dinner" feature - instead of saving data to the database, the web app publishes an event to a message queue:
That decouples the web app from the database, lets you scale components at different levels and deploy them at different times, and it also lets you add new functionality by adding new components listening for the same messages.
It's a great enabler, but it took a little bit of rework to the Nerd Dinner codebase to get there. These are mostly low-risk structural changes, because the original application was a physical monolith as well as a logical one - all the code is in a single C# Web project.
In Chapter 5 there are multiple projects, these are the main ones for now:
NerdDinner.Model is the Entity Framework model, broken into its own class library so I can use it in different components
NerdDinner.Messaging isolates all the message queue logic, and it also contains POCO versions of the core Nerd Dinner entities, so other components can use them without taking a dependency on EF
These are the sorts of changes you'll often need in legacy codebases, where the project structure doesn't allow for re-use. They're not big changes though - and any issues you introduce are likely to be found at compile time, because you're just moving code around.
The Dockerfile for this evolution of Nerd Dinner contains all the extra features I've walked through so far. The IIS logs are redirected to Docker, there's a healthcheck which tests if the app is healthy, and it's a multi-stage Dockerfile.
The approach for the builder stage is different from previous examples - there's a single Docker image which builds all the components for the app. It shows another option for building projects with Docker, and I'll cover it in a later post.
The most obvious change to the code and the Dockerfile is the use of environment variables for configuration. There's an Env class in the code that I use to read in values for the message queue URL and the database connection strings:
ENV BING_MAPS_KEY="" ` IP_INFO_DB_KEY="" ` MESSAGE_QUEUE_URL="nats://message-queue:4222" ` AUTH_DB_CONNECTION_STRING="Data Source=nerd-dinner-db..." ` APP_DB_CONNECTION_STRING="Data Source=nerd-dinner-db..."
This works fine, but I've changed how I do this now. I prefer to stick with the standard .NET configuration system, and use symbolic links with Docker config objects and Docker secrets. That way the code stays true to the .NET way of doing things, but still integrates nicely with Docker.
I do this in my Modernizing .NET Apps - for Developers YouTube series, and cover it in more detail in my Pluralsight course Modernizing .NET Framework Apps with Docker. You can get the gist from this startup script.
This week's image doesn't work on its own, because it needs a message queue to connect to and publish events. I use NATS for messaging, which is a fantastic production-grade OSS message queue, with a Windows variant of the official Docker image.
I'll cover building and running the full solution at the end of this chapter's Dockerfiles.
Next week it's the other save of the "save dinner" workflow, a message handler which listens for events published by the web app. ch05-nerd-dinner-save-handler uses the EF model extracted from the web app, and runs the save logic that the web application used to do.
This makes the whole save workflow asynchronous. The web containers and handler containers can be scaled independently, with the message queue there to store events if the incoming request rate is too high for the handlers to process.