Understanding docker - playing with ruby containers
docker-compose... When you first get into the docker world it can be a bit daunting to understand how all of these tools work and relate to each other.
In this post, I'll be playing around with a simple ruby container just using docker commands. No
docker-compose for now. Just the basic docker commands.
It sure helped me really grasp the basic concepts behind images and containers in isolation before moving into more complex concepts.
Images vs Containers
In docker, it all begins with an image. The image sets the minimum base for a container to run. An image is an immutable file that will serve as a snapshot for the container, and the container will be the actual process on which you can work.
It might help to look at it with a programming mindset - think of an image as a class and the container as an instance of that class. A container is an instance of an image.
For the purpose of this exercise, I want to run a simple ruby sandbox with
irb. This sandbox will be available in a container but for this container to run I'll need its snapshot - the ruby image.
Luckily, we don't have to build the ruby image ourselves. There are plenty of ready-made images available on Docker hub that you can use. If you search for
ruby you'll find the official ruby image in different versions.
docker run command
The command that starts a container process from an image is
Let's understand how the command works by starting a ruby container.
If you run
docker run ruby for the first time, it will first fetch the ruby image in its latest version from the docker hub and download it into your local docker.
You'll get an output similar to this one:
Unable to find image 'ruby:latest' locally latest: Pulling from library/ruby 4c25b3090c26: Pull complete 1acf565088aa: Pull complete b95c0dd0dc0d: Pull complete 5cf06daf6561: Pull complete 942374d5c114: Pull complete 420c5b579440: Pull complete cdfe7730cd5c: Pull complete bfeceb400e58: Pull complete Digest: sha256:8849efdb1f006c5d7b26980f3aeb15f00fa2b5428cfbef8761aef5fc87491b89 Status: Downloaded newer image for ruby:3.0.2 Switch to inspect mode.
Ok, it looks like nothing happened yet but it does say that it downloaded the ruby image successfully. How can I confirm that?
docker images command
If you run
docker images you will see that you'll have the ruby with the
latest tag available for you. You should see something like this:
REPOSITORY TAG IMAGE ID CREATED SIZE ruby latest b28f54b6ce55 3 days ago 881MB
Note that the created timestamp you see in the docker images list is the date that the image was added to the docker library and not the date of the download to your local docker.
Going back to our command, it should have also started a container based on this image but where is it?
docker ps command
docker ps to list all the running containers.
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
The list is empty. Why? The run command should have started a new container process.
If you run
docker ps -a where
-a is the flag to show all containers (running and exited), your container will be there.
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 948b3179783a ruby:latest "irb" 10 seconds ago Exited (0) 5 seconds ago eager_snyder
Look at the Status column. It reads
Exited (0) 5 seconds ago. But why was this container exited?
You may not have noticed, amongst the printed download messages, but there was an error message at the end. Let's see that message again by re-running the docker run command:
-> docker run ruby Switch to inspect mode.
This is because the ruby image by default sends
irb as a command to the container. Once the container process is opened the
irb command should run. But the container cannot run
irb unless we tell it to run in the interactive terminal mode.
In summary: the process was opened, the container ran
irb, got the error message, the process was closed right after it, exiting the container with it.
We can run any command with the interactive terminal mode using the
-> docker run -it ruby irb(main):001:0>
Yeah, our sandbox is running! 🎉
Go ahead and play with ruby a bit.
irb(main):001:0> ['hello', 'world'].join(', ').capitalize => "Hello, world"
Before you exit
irb, open another terminal window and run
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 2ef882ad2b5b ruby:latest "irb" 4 minutes ago Up 4 minutes elated_satoshi
There it is! We have a running container!
irb and run
docker ps again. It's empty. If you run
docker ps -a you should see your container there. It has an exited status again.
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 2ef882ad2b5b ruby:latest "irb" 6 minutes ago Exited (0) 47 seconds ago elated_satoshi 948b3179783a ruby:latest "irb" 15 minutes ago Exited (0) 15 minutes ago eager_snyder
We've just learned another essential aspect of containers. The container has one main process. If that process stops, the container stops. When we left
irb, the process stopped and the container was exited.
Note also that we have two stopped containers. The first container with the random name
elated_satoshi is the one we've just opened and closed after playing with
irb, and the last one
eager_snyder is the container that was created when we ran the command without the interactive terminal flag.
So, every time you run the ruby image, you're creating a different container.
Passing other commands to the container
We've seen that running
docker run -it ruby is the same as running
docker -it ruby irb. We omit it because that's the default for ruby, but we can pass other commands for the container to run once it's opened.
For instance, if we run
docker run -it ruby bash we will create an interactive bash shell in the container.
docker run -it ruby bash root@b1c73a63eef9:/# ls bin boot dev etc home lib lib64 media mnt opt proc root run sbin srv sys tmp usr var root@b1c73a63eef9:/# ruby -v ruby 3.0.2p107 (2021-07-07 revision 0db68f0233) [x86_64-linux] root@b1c73a63eef9:/#
Stopping and removing containers
As we saw earlier, every time you run the
docker run command a new container will be created. This means that with time you'll get a big list of stopped containers:
If you'd like to remove stopped containers, you can just run
docker rm <container id>.
If the container is still running you might have to use the
kill command before removing it.
docker stop <container id> docker rm <container id>
If you'd like to avoid exited containers to be kept after the process is closed there's a useful flag you can use with
docker run, the
docker run -rm -ti ruby
Keeping container processes open
Remember that every time we left
bash in the previous exercises, we closed the process and the container.
But there's a way you can exit yourself from the process without closing it. If you use
ctrl+q, you will no longer see the process but it will still be running in the background. If you run
docker ps -l, where the
-l flag will get the latest container, you'll see your container there.
If you want to start a new container in the detached mode you can pass the detach flag,
-> docker run -d -ti ruby e68f13762ee83ea315b3d0a679432dd1963f276deb387aa3bdce9b4801f21979
It prints out the
container id for you, but again, you can also confirm that there's a new process running with
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES e68f13762ee8 ruby:latest "irb" 4 seconds ago Up 3 seconds crazy_varahamihira
If you want to jump in the process again, you can run the
docker attach <container id> command.
-> docker attach e68f13762ee8 irb(main):001:0>
You can also add a new process to the same container, with the
exec command. Imagine you have
irb running in the background but you want to jump in the bash:
docker exec -it e68f13762ee8 bash root@e68f13762ee8:/#
As we noticed earlier, a container has a name but if you don't give it one, a random one will be assigned to it.
If you want to give it a name, you can pass it in a
docker run --name ruby-sandbox -ti ruby`
And now run
docker ps to make sure there's a container with that name:
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES aee3cd98a190 ruby:latest "irb" 10 seconds ago Up 9 seconds ruby-sandbox
First master the fundamentals!
A lot of commands were covered in this article but I believe that there is only a handful of fundamental concepts in docker. The goal of this article was to share a bit of the process and exercises I've gone through to understand what docker really is. It's a compilation of stuff I was first taught by my colleague Leandro (he has great content on this topic, go check him out) and that I also got from basic docker courses. Have fun! 🐳