Understanding docker - playing with ruby containers
Tags: docker, ruby
Published: 2021-09-16 19:49:35

### 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 an OOP 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](https://hub.docker.com/) 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:

```shell
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:

```shell
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.

```shell
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.

```shell
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:

```shell
-> 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.

```shell
-> docker run -it ruby
irb(main):001:0>
```

Yeah, our sandbox is running!  🎉

Go ahead and play with ruby a bit.

```shell
irb(main):001:0> ['hello', 'world'].join(', ').capitalize
=> "Hello, world"
``` 

Before you exit `irb`,  open another terminal window and run `docker ps`

```bash
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.

```shell
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. 

```shell
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. 

```shell
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`.

```shell
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`:

```shell
-> 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`.

```shell
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.

```shell
-> 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:

```shell
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.

```shell
docker run --name ruby-sandbox -ti ruby`
```

And now run `docker ps` to make sure there's a container with that name:

```bash
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](https://leandronsp.com/) (he has great content on this topic, go check him out) and that I also got from basic docker courses. Have fun! 🐳