Docker Explained: A Gentle Introduction for Developers
For the longest time, Docker was a tool that many developers used sparingly. It's easy to think, "I can just run all my services locally on my machine, right?" This thinking, however, misses out on a core, foundational piece of technology that is essential for modern development.
In this article, we're going to cover virtualization and containerization and clarify the difference between the two concepts, which can often be a point of confusion. We're also going to cover Dockerfiles, images, and Docker containers, and how they all fit into the grand scheme of Docker. This article is intended as a gentle introduction to Docker, where we'll just be scratching the surface.
Why Is Docker Important to Learn?
So, why is Docker an important technology to learn in the first place? You probably use containers and containerization technologies every single day of your career, even if you don't realize it.
Containers are a way to build reproducible, lightweight environments for processes to run. We use them everywhere in continuous integration and continuous deployment (CI/CD) pipelines, like on GitHub Actions. We also use them whenever we're deploying to a server. If you deploy something to the cloud, chances are you're interacting with container technology somewhere along the way. It's a pivotal technology to learn in your web development career.
What is a Container, Anyway?
To talk about containers, we should first talk about virtualization. These two concepts are very closely related, and it's important to understand the distinctions between them.
Understanding Virtualization
Let's break down how virtualization typically works.
You start with a host machine. This could be your local PC, a server in the cloud, or a machine in a data center—whatever it is, it's a piece of physical hardware. This hardware has resources that control how it works: - CPU (Central Processing Unit) - Memory (RAM) - I/O (Input/Output, like your hard drive)
In virtualization, we take small pieces of each of these hardware components and separate them out into a completely separate, isolated machine. This is a virtual machine (VM). Inside this virtual machine, we run a full, entire guest operating system.
This technique is commonly used in the cloud. If you're deploying something to an AWS EC2 instance, you're typically spinning up a new virtual machine to deploy your code onto.
Virtual machines are managed by a special type of program called a hypervisor. The hypervisor is in charge of the VM lifecycle: it starts them, stops them, creates them, deletes them, and provisions their resources. Common hypervisors you might be aware of include VMware or VirtualBox.
Understanding Containerization
Now, let's talk about containerization, which is similar but differs from virtualization. In a container setup, you also have a host PC. Let's say on this host, we want to run a set of processes in isolation so they don't interfere with anything else on the system.
We can achieve this using several techniques built into the host operating system's kernel.
- chroot
: This command can create a new root directory for a process, so it can only live inside that directory and cannot access anything outside of it, like other user directories.
- rlimit
: This kernel feature can limit the amount of resources (like CPU and memory) that these processes can consume.
These techniques, among others, encompass what containerization is. You could do all this manually, but it's difficult and tricky. This is where programs that help manage the lifecycle of your containers come into play.
This is where Docker shines. Docker is a program that manages the lifecycle of containers—it helps you create, edit, run, and interact with them.
To sum up, containerization is the ability to create a lightweight environment where processes can run on a host operating system. They share the same OS kernel but cannot touch anything outside of their little bounded box.
Getting Hands-On with Docker
We've talked enough about the theory. Let's see containerization through Docker in action.
First, you need to install Docker. There are examples of how to install it on the official Docker website. For instance, on Arch Linux, you might run:
pacman -S docker
Once installed, how do we know that Docker is running correctly? Docker provides a helpful command to get a first taste of what it can do for us.
docker run hello-world
After running this, a lot of text appears. Let's go through it line by line to see what Docker is telling us it did.
- Unable to find image 'hello-world:latest' locally: Docker first checked if the
hello-world
image was already on your machine. An image is the blueprint we run our containers from, andlatest
is the default version tag. - Pulling from library/hello-world: Since it couldn't find the image locally, Docker pulled it from a repository. By default, Docker pulls known images from Docker Hub (
hub.docker.com
), a massive registry of pre-built images. If you wanted a PostgreSQL database, for example, you could find an officialpostgres
image there and use it directly. - Downloaded newer image for hello-world:latest: The pull was successful.
- Hello from Docker!: This is the output from the container that was run from the image. The message explains the steps Docker took:
- The Docker client contacted the Docker daemon.
- The daemon pulled the
hello-world
image from Docker Hub. - The daemon created a new container from that image, which runs the executable that produces the output you are seeing.
- The daemon streamed that output to the Docker client, which sent it to your terminal.
Dockerfiles: The Blueprint for Images
You might be asking, what is an image, and how does Docker know how to build them? Before we can build images and run containers, we must first talk about the Dockerfile.
A Dockerfile is a simple text file with instructions for building a Docker image. Let's imagine a project with the following structure:
.
├── Dockerfile
└── coffee-recipe/
├── prepare-beans.sh
└── brew-coffee.sh
Here is a contrived example of what the Dockerfile
might contain:
# Use the latest Ubuntu as our base image
FROM ubuntu:latest
# Run commands on the image to install a package
RUN apt-get update && apt-get install some-contrived-package
# Copy files from our local directory into the image
COPY coffee-recipe/ .
# Run a script that now exists inside the image
RUN ./prepare-beans.sh
# Set the default command to run when a container starts
CMD ["./brew-coffee.sh"]
Let's break this down:
- FROM ubuntu:latest
: This specifies the base image to build upon. We are starting from the official Ubuntu image.
- RUN ...
: This tells Docker to execute a command on the image. Here, we're updating the package list and installing software.
- COPY coffee-recipe/ .
: This copies the coffee-recipe
directory from our local filesystem into the image.
- CMD ["./brew-coffee.sh"]
: This sets the default command that will be executed when a container is started from this image. This can be overridden from the command line.
The general workflow is as follows:
1. You create a Dockerfile with instructions.
2. You run docker build
, which reads the Dockerfile and creates a Docker Image. Let's say we name it coffee:latest
. This image is like a snapshot of the file system for the container, and it's immutable. If you want to make a change, you modify the Dockerfile and build a new version of the image.
3. You run docker run coffee:latest
, which starts a Docker Container from the image. The container will execute the default CMD
, which was ./brew-coffee.sh
.
That, in a nutshell, is how Docker sets up Dockerfiles, images, and containers.
A Real-World Example
Now that we have our core concepts in place, let's try a simple but real-world example. Let's say I have a directory called docker-example
with two files: Dockerfile
and print-message.sh
.
First, let's look at print-message.sh
:
#!/bin/bash
phrases=(
"May the Force be with you."
"I am your father."
"It's a trap!"
)
# Select a random phrase
random_phrase=${phrases[$RANDOM % ${#phrases[@]}]}
# Print it with figlet
echo "$random_phrase" | figlet
This is a simple bash script that randomly selects a phrase and prints it as large ASCII art using a program called figlet
. What if I don't have figlet
installed on my local machine? That's okay—that's what the Dockerfile is for.
Here is our Dockerfile
:
FROM ubuntu:latest
RUN apt-get update && apt-get install -y figlet wget
RUN wget http://www.figlet.org/fonts/starwars.flf -O /usr/share/figlet/starwars.flf
COPY print-message.sh /print-message.sh
RUN chmod +x /print-message.sh
CMD ["/print-message.sh"]
This file does the following:
1. Starts from an Ubuntu image.
2. Installs figlet
and wget
.
3. Uses wget
to download a Star Wars font for figlet
.
4. Copies our local script into the image.
5. Makes the script executable.
6. Sets the script as the default command.
Now, from within the docker-example
directory, we can build our image:
docker build -t ascii .
The -t ascii
flag tags (names) our image ascii
. The .
tells Docker to use the current directory as the build context. You'll see a lot of output as Docker executes each step in the Dockerfile.
Once it's finished, we can see the images built on our system:
docker images
You should see ascii
with the latest
tag in the list. Now, let's run it:
docker run ascii
The container starts, runs the script, and prints out awesome ASCII art to our screen. We can keep running the command, and it will randomly select another saying.
Updating Our Application
But now, let's say we want to modify the script to print different things. Remember, images are immutable. To update, we must edit our source files and build a new image.
Let's change print-message.sh
to have different phrases:
#!/bin/bash
phrases=(
"This is fun."
"I love Docker."
"Cool!"
)
# ... rest of the script is the same
Now that we've changed the script, we'll build a new image. This time, we'll give it a different tag to distinguish it.
docker build -t ascii:different .
This command builds a new image named ascii
but with the tag different
. If we run docker images
again, we'll see both ascii:latest
and ascii:different
.
Now, let's run our new version:
docker run ascii:different
This will output the new phrases we added. The cool thing is that the previous version is still available. We can run the original one at any time:
docker run ascii:latest
This demonstrates a powerful concept: images are immutable snapshots. You don't change them; you just create new, versioned ones.
What We've Learned
We've learned a great deal here. We've covered the difference between virtualization and containerization and explored how Dockerfiles, images, and containers all relate to one another.
This is just the surface of what's possible. There's much more to learn about Docker, such as Docker Compose for multi-container applications, mounting volumes for persistent data, and port mapping for networking.
Join the 10xdev Community
Subscribe and get 8+ free PDFs that contain detailed roadmaps with recommended learning periods for each programming language or field, along with links to free resources such as books, YouTube tutorials, and courses with certificates.