Docker is a containerization system that enables developers and teams to handle application deployment and scaling in a much easier way.
Just as the name suggests, we can think of a container as an isolated system that contains everything needed to run a certain application given a certain setup.
Images can be pre-built, retrieved from registries, created from already existing ones, combined together,started via a common network, etc.
We will see the difference between an image and a container, how they relate to each other, and how these two concepts allow us to make an application self-contained and ready to be deployed.
We can think of images and containers as being two different states of the same underlying concept. Essentially, we can say that a container is a running instance of an image that packages and represents a certain application. For the sake of analogy, let’s go back to Java classes and objects. A container can be seen as an object and an image can be seen as the class.
Images are used to start-up containers. From running containers, we can get images, and all of this can be composed together to form a very powerful, system-agnostic way of packaging and deploying applications. I like to think of it almost as “shipping your localhost.” Why? Since Docker requires that all the configurations needed to run an application are contained and installed within an image, creating an image is essentially replicating the current running state of both the environment and the app, bundling it together, and making it available in the cloud.
So, as a recap:
Images can be retrieved from registries that contain multiple pre-built images to be used by developers to build more complex images
Containers can be seen as running instances of images. It is possible to orchestrate multiple containers to be started and stopped as a “bundled service,” which effectively allows entire applications to be structured as a cluster of related Docker containers
In order to dockerize an application, the flow we follow is to create a Docker image that builds on top of an already existing one and contains, for example, our application JAR or the startup script for a Python app as its startup command.
A startup command for a container is the command that will be executed when the container is started. So, essentially, the running instance of our image becomes the container running our application.
A Dockerfile is a specification that we use to build a new container from an already pre-built image and add our custom logic to start our application. From a Dockerfile, we use the docker build command to build an image.
A Dockerfile works in layers. The first layer starts with the FROMkeyword and defines which pre-built image to use to build our own image. Afterward, we can define more things like user permissions, which files to copy into the container from the local system (think, for example, the requirements.txt from a Python web app), and which startup scripts to execute. Below is an example of a Dockerfile:
FROM python:3
WORKDIR /usr/src/app
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD [ "python", "./your-daemon-or-script.py" ]
Let’s go over the contents of this simple Dockerfile and see what’s happening.
The first line, FROM python:3
, is declaring that the image we are going to build will expand on the base image that contains all the necessary runtime to run Python 3 based applications.
All the commands after this one will be applied on top of the already existing Python 3 image and, as a result, we will create our own Python 3 custom image that contains our application.
WORKDIR
defines the working directory. The specified directory will be assumed for any other command that follows in the Dockerfile and where our application is. In the CMD
and COPY
commands below, the .
directory reference refers to the WORKDIR
.
Afterward, the COPY
command is used to copy files from our local filesystem, where the Dockerfile is, into the container so that these files are readily available in our image.
The RUN
command is self-descriptive and executes a certain command, which, in this example, is installing our dependencies.
Finally, the CMD
command defines the startup command to run when the container starts; in this case, we will simply start our application.
These base concepts are the foundation upon which Docker builds further capabilities. Once you know these concepts, you can learn anything else. You can now build your own images and be able to understand what you’ll see when you encounter a Dockerfile in a Docker project.
Last but not least, you can find many pre-built official images at Docker Hub.
Next, we will see how to package a simple Springboot application with Docker, and we’ll introduce the docker-compose command and the docker-compose
file.