Understanding docker - playing with ruby containers

docker

ruby

Sep, 2021

Understanding docker - playing with ruby containers

Understanding the basics of docker and docker commands is the first step into app containerization.

Docker, Dockerfile, 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 Dockerfile nor 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.

The docker run command

The command that starts a container process from an image is docker run.

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?

The 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?

The docker ps command

Run 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 -it flag.

-> 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 docker ps

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!

Now exit 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 stop or 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 -rm.

docker run -rm -ti ruby

Keeping container processes open

Remember that every time we left irb or 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+p + 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, -d:

-> 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 docker ps.

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:/#

Naming containers

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 --name flag.

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! 🐳

Subscribe below to get future blog posts

No spam, no ads. Unsubscribe any time.