Using volumes for configuration and state
Application state is an important consideration when you're running applications in containers. Containers can be long-running, but they are not intended to be permanent. One of the biggest advantages with containers over traditional compute models is that you can easily replace them, and it only takes a few seconds to do so. When you have a new feature to deploy, or a security vulnerability to patch, you just build and test an upgraded image, stop the old container, and start a replacement from the new image.
Volumes let you manage that upgrade process by keeping your data separate from your application container. I'll demonstrate this with a simple web application that stores the hit count for a page in a text file; each time you browse to the page, the site increments the count.
The Dockerfile for the dockeronwindows/ch02-hitcount-website image uses multi-stage builds, compiling the application using the microsoft/dotnet image, and packaging the final app using microsoft/aspnetcore as the base:
# escape=`
FROM microsoft/dotnet:2.2-sdk-nanoserver-1809 AS builder
WORKDIR C:\src
COPY src .
USER ContainerAdministrator
RUN dotnet restore && dotnet publish
# app image
FROM microsoft/dotnet:2.2-aspnetcore-runtime-nanoserver-1809
EXPOSE 80
WORKDIR C:\dotnetapp
RUN mkdir app-state
CMD ["dotnet", "HitCountWebApp.dll"]
COPY --from=builder C:\src\bin\Debug\netcoreapp2.2\publish .
In the Dockerfile I create an empty directory at C:\dotnetapp\app-state, which is where the application will store the hit count in a text file. I've built the first version of the app into an image with the 2e-v1 tag:
docker image build --tag dockeronwindows/ch02-hitcount-website:2e-v1 .
I'll create a directory on the host to use for the container's state, and run a container that mounts the application state directory from a directory on the host:
mkdir C:\app-state
docker container run -d --publish-all `
-v C:\app-state:C:\dotnetapp\app-state `
--name appv1 `
dockeronwindows/ch02-hitcount-website:2e-v1
The publish-all option tells Docker to publish all the exposed ports from the container image to random ports on the host. This is a quick option for testing containers in a local environment, as Docker will assign a free port from the host and you don't need to worry about which ports are already in use by other containers. You find out the ports a container has published with the container port command:
> docker container port appv1
80/tcp -> 0.0.0.0:51377
I can browse to the site at http://localhost:51377. When I refresh the page a few times, I'll see the hit count increasing:
Now, when I have an upgraded version of the app to deploy, I can package it into a new image tagged with 2e-v2. When the image is ready, I can stop the old container and start a new one using the same volume mapping:
PS> docker container stop appv1
appv1
PS> docker container run -d --publish-all `
-v C:\app-state:C:\dotnetapp\app-state `
--name appv2 `
dockeronwindows/ch02-hitcount-website:2e-v2
db8a39ba7af43be04b02d4ea5d9e646c87902594c26a62168c9f8bf912188b62
The volume containing the application state gets reused, so the new version will continue using the saved state from the old version. I have a new container with a new published port. When I fetch the port and browse to it for the first time, I see the updated UI with an attractive icon, but the hit count is carried forward from version 1:
Application state can have structural changes between versions, which is something you will need to manage yourself. The Docker image for the open source Git server, GitLab, is a good example of this. The state is stored in a database on a volume, and when you upgrade to a new version, the app checks the database and runs upgrade scripts if needed.
Application configuration is another way to make use of volume mounts. You can ship your application with a default configuration set built into the image, but users can override the base configuration with their own files using a mount.
You'll see these techniques put to good use in the next chapter.