From Virtual Machines (VMs) to Docker
I used to reach for VMs to solve this problem. It made managing my various projects’ dependencies and environments a breeze. The VMs helped to ensure consistent behaviour between my home and work machines.
However, VMs aren’t all glitz and glamour. They require more overhead. VMs can take several minutes to create and launch whereas Docker containers can be created and launched in just a few seconds since they’re much smaller. This also means that an application in a Docker container can run anywhere up to twice as fast as in a virtual machine and more containers can share a host because of the shared OS not being duplicated across each VM.
This was more than enough to get me excited to learn more about Docker. And, if you’re reading this, I’m sure you’re already convinced, too.
What is Docker?
Docker is a tool we can use to isolate and containerize our application and the environment in which it runs from that of our host machine. This allows us to also version our entire "stack" and all the requirements and dependencies for the application to run within version control.
Why is Docker so great?
Docker is great for a plethora of reasons, but personally, the main advantage is how easily Docker can be run locally and deployed in production, and how it helps ensure that what works on my machine will work on every machine.
Note: I have run into instances where some of the terminal commands run to execute/build docker commands/images produce some errors when running on Windows, but with minor testing and tweaking, cross platform compatibility can be achieved.
What does Docker solve?
It eliminates the "well, it works on my machine" excuse from your vocabulary. It also helps ensure that when a new developer is on-boarded to your project, once they have git and docker installed, they will be ready to run your application which minimizes the staff onboarding time.
For our demo, we will be aliasing npm commands to Docker commands to make the development cycle more familiar to developers, and so, NPM is a dependency on the host machine, simply to kick start the docker commands.
Fear not the Terminal
To get started with this demo, you should have Docker installed on your host machine. Additionally, as mentioned above, in parts 2 and beyond, we will require npm (v6.4.x or later) and node (v8.11.x or later) on your host machine.
This was written and executed on MacOS. If you notice any commands that do not work for Windows, please leave a comment to help out other readers. I'll try to update my notes here too.
A quick Google search for "install docker" eventually takes you here: . The installation steps have changed (in terms of where to find the download) a few times. I believe you will need to register for an account with Docker (and/or Docker Hub) but don't worry, we'll be using these later in the series to publish our images to a Docker repository.
Once you've completed the installation, join us back here and we'll review a handful of commands we'll be using over and over to make our lives easier.
Note on Terminology: We'll be using terms like build, image, and container a lot in this article, so I wanted to define them upfront.
The build is the process of taking the steps outlined in the Dockerfile (more on that later) to create your image.
The is like an onion, built up in layers from your Dockerfile. This is your primary asset.
Similar to a class definition, the container is an instance of your image. A container is to an object, like an is to a class. You can have many containers of the same image if required.
So now docker should be installed and running. You can confirm this by running docker -v in your terminal. At the time of this article, I am using Docker version 18.09.0, build 4d60db4. You don't need this version, but later in this series, I am planning to use some of the newer features. I'll try to comment on those sections where a specific version (or newer) would be required.
Running docker in your terminal will give you a long list of commands you can use to manage your Docker assets. Let's look at a few:
Returns a list of running containers
$ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
Ok, so we see ... nothing. That's because we have no containers running. Let's run our first container using a predefined image we can pull from Docker's repository. It's time for hello-world!
Using default tag: latest latest: Pulling from library/hello-world d1725b59e92d: Pull complete
This command allows you to pull prebuilt images from repositories like Docker Hub. Let's see all the images we have loaded on our machine.
REPOSITORY TAG IMAGE ID CREATED SIZE hello-world latest 4ab4c602aa5e 3 months ago 1.84kB
This shows us a list of built images that we have on our machines. You can easily bring up any of these in a container using the command we will look at next: docker run [IMAGE]:[TAG|'latest']
If the image does not exist locally, it will pull, and run a container of the image provided
$ docker run hello-world Hello from Docker! This message shows that your installation appears to be working correctly.
So what happened here? Basically, Docker looks at your local list of images. Since we pulled this image from Docker hub in the last step, Docker found the hello-world image locally. Docker then created a container running this image.
If you read the output from our hello world, they even recommend what to try next.
To try something more ambitious, you can run an Ubuntu container with:
$ docker run -it ubuntu bash
Let's see what happens.
Unable to find image 'ubuntu:latest' locally latest: Pulling from library/ubuntu 32802c0cfa4d: Pull complete da1315cffa03: Pull complete fa83472a3562: Pull complete f85999a86bef: Pull complete Digest: sha256:6d0e0c26489e33f5a6f0020edface2727db9489744ecc9b4f50c7fa671f23c49 Status: Downloaded newer image for ubuntu:latest root@3fab2218a261:/#
Now we're inside our Docker container! Crazy right? Try out some simple commands, muck around. Doesn't matter. We'll review how to kill the container shortly. Right now, nothing inside your container is linked to your file system so anything you do in here will not be persisted.
Go play! I'll wait...
Ok. So, before you $ exit your container, let's open a new terminal to see a little more of what's going on in your new container. Run the docker ps command again and you should see an entry like this:
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 3fab2218a261 ubuntu "bash" About a minute ago Up About a minute elated_tesla
We have 2 unique identifiers here that we should take note of, the CONTAINER ID and the NAME. This will allow us to get back into our container from another terminal window. We can try that as we review the next command..
With this Docker command, we can execute a command against our running container, and we can even tell Docker that we want to remain interactive -i and we want to open a terminal -t using flags in the docker exec command. So, take the CONTAINER ID or the NAME from your docker ps command, and let's get back into our running container by running docker exec with the -it flags.
$ docker exec -it elated_tesla bash root@3fab2218a261:/#
And we're right back in the terminal of our container! Pretty cool, eh? Ok, let's start cleaning up. Simply exit out of the container in our second terminal window running $ exit.
Let's see the command we would use to stop our container if we weren't logged into the terminal. This is how you will be stopping containers that are running in detached -d mode.
Note on Flags: If you read the docker run section above, you’ll see these same flags (it) used. This allowed us to run our container, and exec against the newly created container and remain interactive with it.
Had we instead used the -d flag in our docker run -d ... command, we would need to use the docker exec -it ... command to gain an interactive terminal.
$ docker stop elated_tesla elated_tesla
Now, if we run docker ps again, we'll see an empty list. Well, this is a little misleading. Since Docker isn't persisting to our host machine, it didn't just kill off everything we've done. The container still exists, but we can't see it. Let's add a flag to our docker ps command.
$ docker ps -a CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 3fab2218a261 ubuntu "bash" 10 minutes ago Exited (0) 43 seconds ago elated_tesla 6088b6cbbab0 hello-world "/hello" 13 minutes ago Exited (0) 13 minutes ago upbeat_robinson
Found em! The problem is, they're still consuming some (not many, but some) resources. Let's fully clear them out. Keep in mind that anything you did inside of the container really will be lost once we fully remove the container, but in this case that is okay. We want a fresh start.
$ docker rm elated_tesla elated_tesla
When we run docker ps -a again, we'll see the container has been removed from our list of stopped containers. However, if we run docker images we will see that ubuntu is still an image we have on our host machine. This just means when we spin up a new container, we won't need to download anything from Docker Hub (unless we use a different tag than the one we have locally.)
Although we won't go into it in this article, if system resources ever become an issue, you can run docker image prune or docker container prune to free up some resources.
Ok! So, we covered off how to pull images, start and stop containers, and remove and execute against them. We're at a pretty good place, so we are ready to start looking at building our own custom Docker images! Stay tuned for part two of this series where we'll dockerize a simple Vuejs app!
Please, feel free to do your happy dance in the meantime ;)