Writing software is hard…
Some parts are harder than others - and for me, the most difficult part about development has always been setting up the environment for whatever software I’m working with.
Whether it’s installing Node Version Manager to make sure I have the right version of node to launch a project I pulled from Github…
Or starting a new Ruby on Rails project on a new machine…
For me, that stuff has always been way harder than actually writing software. And if you’ve ever felt the same way about setting up environments for your projects, then learning Docker is going to be the best decision you make for your developer career.
But what exactly does Docker do? At a first glance, Docker just sounds like another tool to learn. But once you understand what problems Docker solves - and how to use Docker to solve those problems - you’re going to feel invincible as a developer.
The big hairy problem every developer faces
When showing you how to use Docker, I’m going to use Ruby on Code. I’ve been using Ruby for 10+ years and it was my first love, so that’s what I’m going to use here. But the concepts work across all languages.
Let’s say I have a project written in Ruby that requires Ruby version 3.1.0. But my system is using Ruby 3.34 - I’m gonna have an issue.
Sure, I could just use a Ruby version manager to install the correct version. But that means if I want to run this project on another machine, I’d have to install the correct version of Ruby on that machine. And, this simple example just requires Ruby, but what about bigger projects that require much more than just a specific version of Ruby?
So I have a small Hello World application that looks like this:
if RUBY_VERSION != '3.1.0'
abort("This app requires Ruby 3.1.0. Please use the correct Ruby version.")
end
puts "Hello world"
If I try to run this script and I don’t have the required Ruby version, I’ll get a message like this:
This app requires Ruby 3.1.0. Please use the correct Ruby version.
Again, this is a simple example, but imagine having a team of 20 developers and much more dependencies - it turns into a huge pain in the butt.
Wouldn’t it be much better - and much nicer to other developers - if you could share some software with other people that just… works? That’s exactly what Docker does for you.
What exactly does Docker do?
Typically when you start working on a project from another developer, you have to find and install all the dependencies for yourself. With Docker, instead of sending some code to another dev and saying “here you go, you figure out the rest,” you pack up everything they need in one go so they can just rub your application right away.
In our simple example, we package up Ruby version 3.1.0 and the code and put it into a box before sharing the application.
How does Docker work?
There are two very important terms to understand when using Docker: images, and containers.
When you package up all the dependencies and the code for the application, you create an image.
Then, you take one of these images and run it into a container.
A container is just an instance of an image. You could even think of it as being similar to classes and instances.
You create a class (an image) and then you can use that class as a blueprint to have as many instances you want (container).
Really think about that for a second… you can create an image and deploy it on multiple servers without needing to worry about setting each server up with the correct version of Ruby (and setting up all the other dependencies.
How to make your own Docker images
So, images sound great, but there is one issue: since images contain everything an application needs to run, you need to send many more files - which requires more storage than just sending over the code for a project.
To solve this problem, we use something called a Dockerfile. Instead of sending an image of your application to another developer, you send them a Dockerfile that has the instructions on how to set up the environment.
It’s essentially like sending over a recipe for another dev to build the image themselves instead of sending the complete image.
Let’s take a look at what our Dockerfile would look like for our example project:
FROM ruby:3.1.0
WORKDIR /app
COPY . .
CMD ["ruby", "app/script.rb"]
For better organization, I put our script.rb file into a directory called app.
Outside of the app directory - at our project’s root directory - we have this Dockerfile. By the way, the Dockerfile has no extension.
So what’s going on in this file?
At the very top, is where the magic is:
FROM ruby:3.1.0
Here, we’re saying, the first thing you need to do before running this application is to install ruby 3.1.0.
Next, with:
WORKDIR /app
We’re saying we want to use the app directory as the root directory when referencing our paths.
With:
COPY . .
We want to copy everything from the current directory - the app directory that we just set - and copy it to the root of our Docker image.
Then then finally we have:
CMD ["ruby", "app/script".rb]
This is simply starting our application. If your application is more complex, it may require more complex commands.
Now that we have this file, we actually have to use Docker to build the image.
You can find step-by-step instructions for installing Docker here.
In the command line we run this command:
docker build . -t ruby-test
Let’s break that down real quick…
docker - tells the command line to use docker
build - the command we want to do. In this case, we want to build an image from our Dockerfile
. - Here, we tell docker where our Dockerfile is. If your Dockerfile isn’t in the current directory, then you need to change the path there.
-t - this option stands for “tag” which means we want to tag our image with a name. And finally….
ruby-test - The tag (name) we give to the image.
After running that command, we see a bunch of stuff in the console like so:
If you look closely, this is following the directions in the Dockerfile. And once you run that command you might notice something…
Nothing changed in your project directory!
That’s because when you build a Docker image, the image gets saved in a special location where all docker images are saved.
Finally, after building the image, you can launch a new container using Docker. I’ll cover this in another tutorial later to keep this tutorial from being a million words long
But wait a second….
The whole point of Docker is make sure when you share your code, you also include all the dependencies the project needs to run.
But when we share a Dockerfile instead of an Image, the Dockerfile has to go out and get all the dependencies. Doesn’t this kind of defeat the purpose?
The last part of the puzzle is Dockerhub.
Docker Hub hosts a ton of different files - including ruby:3.1.0. When you run your Dockerfile using the build command, Docker goes to Dockerhub to get the required files.
That means everyone that runs this Dockerfile gets all the required dependencies from the same location - making sure that not only do you have all the dependencies your project needs to run, but that all the versions of all the dependencies are using the exact same versions from the exact same place.
And it’s that last part that pulls everything together.
Let’s recap
We write a Dockerfile that includes the recipe for all the dependencies our application needs.
When we build an image using the Dockerfile - regardless of whose machine it’s on - we use all the same exact versions of all the dependencies from the same source.
We can take that image and run it in multiple containers - basically taking our image (class) and creating multiple containers (instance).
This makes sure wherever we run our software, we have everything we need.
Congratulations, you’ve (hopefully) solved the biggest problem in software engineering.
Top comments (0)