Docker containers should be designed to stateless, meaning that they can survive system reboots and container terminations gracefully, and without the loss of data. Designing with stateless containers in mind will also help your app grow and making future horizontal scaling tasks trivial.
Let's review our setup. Here, we have a small Express app that uploads files to the local file system. Note that the base route serves up an HTML form with file inputs, and the upload route handles moving or uploaded file into a folder on the file system named uploads.
The Docker file for this app is simple. We are simply setting up our current working directory, copying over assets, making an uploads directory, running Yarn to install prerequisites, exposing port 8080, and then starting our web server.
We are using Docker compose to run our containers. For our main app service, we will simply build our app from the local directory's Docker file, and also bind port 8080 from the app to the host. We will start our app up with docker-compose up. Let's test out the file upload functionality by uploading an image of our cat friend.
We can see that Herman is successfully uploaded to the uploads folder, but we have a small problem here. Let's stop our app, remove our containers, and start our app back up. Then let's refresh our browser window.
Herman did in fact die when our container died. This presents two problems, one being that we will lose all our uploaded content if a container is killed off, the other being that no one likes dead kitties. This shows that Docker's file system is ephemeral, and that we need to design systems that can persist container terminations.
Our entire fleet of containers should be able to kill off at any given moment and redeploy at any time without losing any data. The easiest way to fix this problem is to set up a persistent volume. Persistent volumes map a host directory into a container directory so that even when containers die, the volume does not.
The directory will remap back to the host when the new containers are deployed. Setting up volumes is really easy with compose. Let's open up our docker-compose YAML file, then add a volumes property. Under this property, we will simply choose any arbitrary name for our persistent volume.
Let's name this App Data. Make sure to suffix the name with a colon. Next, let's go into our app servers and add a volumes property. Since there can be many volumes set up, we prefix our volumes entries with a dash.
Then we specify the name of the volume we want to use, in this case, App Data. Let's precede that with a colon, and then specify a folder to be persistent, in this case, /serv/uploads. When the container starts, this volume path will be mounted from our volume into the container at this directory.
This is enough to persist data between container deletions and respawns. Let's remove our current app containers to ensure we are running from a clean state, then start our app again with compose. We will reupload our cat friend Herman and follow the link to ensure he is uploaded.
Now, let's stop our app, and then completely kill off our containers and Herman again with docker-compose rm-f. At this point in time, Herman is officially a Schrödinger's cat, because he is currently both dead in our containers, but also alive and well within our boxed volume.
Let's take him out of the box, and start our app back up with docker-compose up. If we refresh our browser, we can see that Herman is alive and well.