Containers for fun and profit!

So you're now officially excited about Docker and containers.

Good! They're an awesome tool to create portable configurations for your software and make sure it works seamlessly across multiple devices.

If you've been around them long enough, though, you're probably using Docker engine's CLI (command line interface) quite often.

Commands like:

docker exec -ti 1x81hs72k2s9 /bin/bash

or

docker start container-name

Are probably all over your shell history.

I had the same problem, and so I made a small hack and uploaded it to my slowly-expanding Useful Snippets repository (which you can totally star if you feel like it :) ):

TomGranot/useful-snippets
Useful things I made/found over time. Contribute to TomGranot/useful-snippets development by creating an account on GitHub.

Money time

Specifically, if you go here you'll see the following snippets:

#!/bin/bash

# Get container id of exiting container
dgrep(){
    docker ps -a | grep "$1" | cut -c1-12 | head -n 1 
}

# bash into an existing container
dbash() {
    docker exec -ti $(dgrep "$1") /bin/bash
}

# sh into an existing container
dsh() {
    docker exec -ti $(dgrep "$1") /bin/sh
}

# Execute something in an existing container
dex() {
    docker exec -ti $(dgrep "$1") $2
}

# Start an existing container
dstart(){
    docker start $(dgrep "$1")
}

# Stop an existing container
dstop(){
    docker stop $(dgrep "$1")
}

# Remove an existing container
drm(){
    docker rm $(dgrep "$1")
}

# Stop and remove an existing container
dsrm(){
    dstop $(dgrep "$1") && drm $(dgrep "$1")
}

# Remove an existing image
dirm(){
    docker image remove $(dgrep "$1")
}

# Get logs of an existing container
dlog(){
    docker logs $(dgrep "$1")
}

These go inside your shell's configuration file, probably located at ~/.bashrc if you're on one of the Linux distros and older macs, or ~/.zshrc if you're on newer macs.

After you put those commands inside your shell configuration file, whenever your shell starts up they will become available for use inside your session - just like regular shell commands.

Shell functions as command aliases

Note that what I did up there was define shell functions that perform specific commands, and take arguments just like regular shell commands.

This goes in contrast to defining shell aliases, like Nick Taylor does here (for the most part), and allows us to pass arguments to our "aliases".

This was a bit of a mouthful. Let's consider, as an example, the dgrep function I defined earlier:

# Get container id of existing container
dgrep() {
    docker ps -a | grep "$1" | cut -c1-12
}

dgrep() is a shell function that accepts an unlimited amount of arguments (as shell functions do). In order to access the arguments provided to the function inside the function's body, we use the $X notation, where X is an integer number.

$0 always refers to the name of the executing command, so if you're running:

my-command first-tom second-tom

And my-command is defined as

my-command() {
    echo $0;
    echo $1;
    echo $2;
}

Then the output will be:

my-command
1
2

Coming back to our dgrep command:

# Get container id of existing container
dgrep() {
    docker ps -a | grep "$1" | cut -c1-12
}

Note that what it's doing is getting the contents of docker ps (that is a list of all running containers), then piping the input into grep, which returns any match to the searched text with the provided argument.

In our case, it's "grepping" for $1, which is the first argument provided to dgrep. It finally pipes that output into cut -c1-12 that gets the first 12 characters of the piped text (which is, coincidentally, exactly the length of Docker container IDs).

For example, if you're running:

dgrep tom-container

Then if tom-container is a running container, docker ps will return a string containing all the information the Docker engine has about that container.

dgrep will then pipe it into the cut command, and print out only the container ID of that container.

But why do I need that?

Excellent question. Remember our example from the beginning of the article, where we used the full container ID to refer to the container?

docker exec -ti 1x81hs72k2s9 /bin/bash

Nobody expects you to remember the full names or the IDs of your containers. We can now re-write the command as:

docker exec -ti $(dgrep substring-in-container-name) /bin/bash

Where $() is a subshell - a special command that executes whatever is provided to it, and sends out the output as text before running the rest of the command.

In our case, $(dgrep substring-in-container-name) will find us a container ID based on some substring in the name (like my in my-very-long-and-hard-to-remember-container-name), and pass it to the docker exec -ti CONTAINER-ID /bin/bash command as CONTAINER-ID.

Wait, but that's still kinda long...

You're right! "Bashing" into a container (i.e. running bash inside that container interactively, which is basically "opening" a terminal to that container) is something we do quite often to debug long-running containers, and to do other fun filesystem shenanigans (ask me about that if you'd like to know more!).

In comes dbash():

# bash into an existing container
dbash() {
    docker exec -ti $(dgrep "$1") /bin/bash
}

As you can see, dbash is calling the previously mentioned dgrep in a subshell, and passes that subshell the argument provided to dbash - namely a substring of the relevant container.

This eventually lets us do something like:

dbash tom

And get a terminal to the first container on the list of running containers that has tom in its human-readable name.

Pretty neat, right?

Conclusion

Containers are fun. And so is working with the shell!

I literally just made a really nice hack using Docker for an open-source project I'm working on with my good friend, Federico. Feel free to take a look if you're into container wizardry!

Side note

An observant reader would note that I can save one pipe by using Docker command formatting. That reader would be correct, but I think my syntax is simpler and easier to explain to beginners, so I stuck with it for this tutorial (and, honestly, for my own ~/.zshrc as well).