Running exercism inside a docker container

TL;DR

exercism.io is a nice place to learn programming languages. It has a command-line tool to download and upload the exercises. In this post, I configure a docker image and run exercism inside a container.

Docker concepts involved

Running exercism inside docker gives us a great opportunity to learn and practice a few docker concepts. I’ve already talked about why I’m using docker as much as possible, even when it’s overkill (like in this example). If you want to know about it, you can read about it in this post.

If you follow the example, you will need to dig deeper into the following docker concepts:

  • Build custom docker images using a Dockerfile
  • Build arguments
  • Differences between images and containers
  • Run containers
  • Bind volumes
  • Bind volumes and file ownership/permissions

Explaining these docker concepts is out of the scope of this post, so I will not go into the details of any of them.

Before you start..

You should be able to run all the commands that I use in this article in the following systems:

  • Windows 10 + WSL2 (I used the Ubuntu 20.04LTS image) and Docker for Windows installed
  • Any Linux distribution with Docker installed
  • Mac OS and Docker for Mac installed

You will also need an account in the exercism.io website to be able to download an exercise.

About exercism.io

Before we start, what is exercism.io? Developed initially by the one and only Katrina Owen, it’s a community-driven site with the goal of helping people improve their coding skills.

Code practice and mentorship for everyone

Level up your programming skills with 3,442 exercises across 52 languages, and insightful discussion with our dedicated team of welcoming mentors.

Exercism is 100% free forever.

I was already registered at a couple of similar websites like CodeWars or the euler project when I discovered excercism.io. But I fell in love with it very fast:

  • I can use my computer to solve the problems so I can work offline and use whatever version I want to use of the tools.
  • Since I work in my local computer, I can track my progress using git. In CodeWars, for example, you have to use their interface whether you like it or not
  • They have mentors that look at your code and give you meaningful feedback
  • The challenges are well thought and designed with different degrees of difficulty
  • The paths in which they organize the exercises make it very easy to track your progress and resume your work whenever you want to

Don’t get me wrong. I like CodeWars and the incredible community of people working on their challenges, but the simplicity, clarity, and mentorship of exercism.io got me real fast. If you have a chance, give it a try.

What I want to achieve

  • Be able to run the exercism executable in my computer inside a docker container
  • Be able to do the exercises of the bash track without installing bash.

Yes, I know that any MacOS or Linux box have bash installed, but what about windows? 💁‍♂️ In this post I will focus on the first step.

The Dockerfile

The exercism.io webpage has a nice walkthrough that you can follow to install the client in Windows, Linux, and Mac OS. We will use the instructions for the Linux walkthrough to create our Dockerfile:


FROM debian:stable

ARG USERID
RUN adduser --uid ${USERID:-1000} --gecos "" --disabled-password exercism && \
    apt-get update && \
    apt-get install -y curl bats vim && \
    curl -L https://github.com/exercism/cli/releases/download/v3.0.13/exercism-3.0.13-linux-x86_64.tar.gz --output exercism-3.0.13-linux-x86_64.tar.gz && \
    mkdir exercism-3.0.13-linux-x86_64 && \
    tar xfz exercism-3.0.13-linux-x86_64.tar.gz -C ./exercism-3.0.13-linux-x86_64 && \
    cp exercism-3.0.13-linux-x86_64/exercism /usr/local/bin/ && \
    mkdir /app && \
    chown exercism /app && \
    apt-get clean

WORKDIR /app
USER exercism

ENTRYPOINT ["/usr/local/bin/exercism"]

Let’s go through the Dockerfile and comment on it.

FROM debian:stable

debian:stable is our base image. It has everything we need to run the exercism executable and an up-to-date bash version (5.0.3).

ARG USERID
RUN adduser --uid ${USERID:-1000} --gecos "" --disabled-password exercism

To avoid running processes as root inside our container, we create an exercism user that we will use to run processes inside the containers created from this image.

If you look at the adduser command above, you will notice that the uid of the user inside the container is passed as a build argument. Later, we will pass the uid of the user in the host computer so the user in the host that builds and runs the image will have the same uid as the user exercism inside the container.

We do this because later we will use bind-volumes to share files between the host and the container. Since both users (the host user and the exercism user) have the same uid, any files created inside the container will belong to the user in the host.

RUN ...
    apt-get install -y curl && \

Install curl (we’ll use it later to download the exercism package from github.com)

RUN ...
    curl -L https://github.com/exercism/cli/releases/download/v3.0.13/exercism-3.0.13-linux-x86_64.tar.gz --output exercism-3.0.13-linux-x86_64.tar.gz && \
    mkdir exercism-3.0.13-linux-x86_64 && \
    tar xfz exercism-3.0.13-linux-x86_64.tar.gz -C ./exercism-3.0.13-linux-x86_64 && \
    cp exercism-3.0.13-linux-x86_64/exercism /usr/local/bin/ && \

Download, extract, and copy the latest version of the exercism client into /usr/local/bin.

RUN ...
    apt-get clean && \
    rm -rf /var/lib/apt/lists/* && \

Clear the apt cache to make our image smaller (more info here).

RUN ...
    mkdir /app && \
    chown exercism /app && \
WORKDIR /app
USER exercism

Create the path that we’ll use to mount the bind-volume later.

ENTRYPOINT ["/usr/local/bin/exercism"]

Prepare the container to be run as an executable.

The Makefile

Once the Dockerfile is ready, we’ll use the make utility to help us run the docker commands. Using your favorite editor, create a new file called Makefile with the following content:

USERID := $(shell id -u)

build:
      DOCKER_BUILDKIT=1 docker build -t exercism:latest --build-arg USERID=${USERID} .

As I mentioned in section The Dockerfile above, we are building the image with a user (exercism) that will have the same user id as our user in the host system. The first line of the Makefile creates a variable USERID to store the user id using the built-in shell command of the Make utility.

In the definition of the build target, we pass the user argument USERID to the build command using the --build-arg option.

Save the file. We will use it to build the image in the next step.

Build the image

From the console run the following command:

> make build

The build will start and after a few seconds you should see an output similar to this one:

DOCKER_BUILDKIT=1 docker build -t exercism:latest --build-arg USERID=501 .
[+] Building 31.9s (7/7) FINISHED
 => [internal] load .dockerignore
 => => transferring context: 2B
 => [internal] load build definition from Dockerfile
 => => transferring dockerfile: 746B
 => [internal] load metadata for docker.io/library/debian:stable
 => CACHED [1/3] FROM docker.io/library/debian:[email protected]:3ac5e3f2fdd73d124e538d0a21f9fa9ba273bebed18af5f38bd87c2e69c04cb5
 => [2/3] RUN adduser --uid 501 --gecos "" --disabled-password exercism &&     apt-get update &&     apt-get install -y curl ...
 => [3/3] WORKDIR /app
 => exporting to image
 => => exporting layers
 => => writing image sha256:c91010d93b29729c2315753f7dec65d4747bdff13d5e611907b7867d2671180a
 => => naming to docker.io/library/exercism:latest

The last two lines are the ones we are looking for: they mean that the image was built and ready to use.

Running the command exercism troubleshoot

Let’s run the exercism troubleshoot command:

> docker run --rm exercism troubleshoot

Troubleshooting Information
===========================

Version
----------------
Current: 3.0.13
Latest:  3.0.13


Operating System
----------------
OS:           linux
Architecture: amd64


Configuration
----------------
Home:      /home/exercism
Workspace: /home/exercism/exercism (default)
Config:    /home/exercism/.config/exercism
API key:   <not configured>
Find your API key at https://exercism.io/my/settings

API Reachability
----------------

GitHub:
    * https://api.github.com
    * [connected]
    * 43.0216ms

Exercism:
    * https://api.exercism.io/v1/ping
    * [connected]
    * 485.9616ms


If you are having trouble please file a GitHub issue at
https://github.com/exercism/exercism.io/issues and include
this information.

If you see this output, your image is ready!

Configure exercism`

Looking at the output of the previous command, we can get a hint of the bind volumes that we need to be able to run the command:

Configuration
----------------
Home:      /home/exercism
Workspace: /home/exercism/exercism (default)
Config:    /home/exercism/.config/exercism
API key:   <not configured>
Find your API key at https://exercism.io/my/settings

We will need two bind volumes. One for /home/exercism/exercism and another one for /home/exercism/.config/exercism:

  • The directory /home/exercism/exercism inside the container will be bound to the directory of the host in which we are running the command, i.e $(pwd).
  • The directory /home/exercism/.config/exercism inside the container, will be bound to the local directory of the host $HOME/.config/exercism so we can save and reuse the configuration between different executions of the container.
> docker run \
      --rm \
      -v $(pwd):/home/exercism/exercism \
      -v $HOME/.config/exercism:/home/exercism/.config/exercism \
      exercism:latest 
      configure --token=11___XXXXXX___ab --workspace=/home/exercism/exercism

You have configured the Exercism command-line client:

Config dir:                       /home/exercism/.config/exercism
Token:         (-t, --token)      11___XXXXXX___ab
Workspace:     (-w, --workspace)  /home/exercism/exercism
API Base URL:  (-a, --api)        https://api.exercism.io/v1      

That looks good!! Now, we should have a new file in our host computer holding the configuration of exercism:

> cat $HOME/.config/exercism/user.json
{
  "apibaseurl": "https://api.exercism.io/v1",
  "token": "11___XXXXXX___ab",
  "workspace": "/home/exercism/exercism"
}

Create an alias

We will create an alias to save us some typing. We add the following alias to the .bashrc, .bash_profile or .zshrc (depending on your Linux distribution or Operating System):

alias exercism='docker run --rm -v $(pwd):/home/exercism/exercism -v $HOME/.config/exercism:/home/exercism/.config/exercism exercism:latest'

This alias will be there the next time you open a new terminal. To make it available in the current shell you can just run the command above to create the alias.

Get our first exercise

We are almost there. Let’s start our Groovy path in exercism.io in mentored mode.

To download the first exercise:

  1. Create a folder $HOME/learning/exercism in which I will do all the exercises
      ~> mkdir -p learning/exercism
      ~> cd ~/learning/exercism 
    
  2. Using the alias that we created in the previous section, we download the exercise (the command I’m using is copied from its page in excersim.io)
      ~/learning/exercism > exercism download --exercise=hello-world --track=groovy
    

After solving the problem, which is out fo the scope of this article, you can submit the solution by running:

~/learning/exercism > exercism submit groovy/hello-world/src/main/groovy/HelloWorld.groovy

Alternatives

Instead of bind mount /home/exercism/.config/exercism, we can store the configuration inside the image. This would be the subject of another post since it requires a different Dockerfile and a way to handle the token appropriately.

References and attributions