Yash Ladha Blog

Personal Dev Environment using Docker


When I was trying to tune my competitive programming setup on Mac, I was bugged by the fact that bits/stdc++.h is not natively present on OSX. To workaround this issue, I have to either use nasty hacks like copying the contents into a local file or symlinking the file in path. In the end, all of those methods will get the work done, but not in the cleanest way possible.

This is just one example of the many inconsistencies we find when our dev environment is not the one on which we want to develop. Lately, I have been using VSCode and found that they have a very cool extension of Remote Docker which lets you connect to any Docker host on the system and install VSCode in that. You can edit the files in that container without leaving VSCode, which is awesome.

Though I am not a big VSCode user and primarily use Neovim as my PDE. I thought why not replicate the same thing but for Neovim instead.

Technically, we can execute any shell program inside Docker container and we will be using this and some other features which Docker provides to achieve this.

Step 1: Select Appropriate Image for use

Decide which image you will be using for your dev environment. Generally, one can go for defacto environment like Ubuntu or Alpine. But you are free to use any image of your choice. For the sake of this blog, I will be going ahead with the Ubuntu system image as it has all the tools required for competitive programming setup.

FROM ubuntu:18.04

Step 2: Install necessary dependencies

After choosing the image, next step is to install the required packages for dev environment, which are not natively present in the image. In my case, I will be using this dev environment for competitive programming, so latest Clang should be good. Not to forget the editor, in my case Neovim binary.

# Make sure the image is updated, install some prerequisites,
# Download the latest version of Clang (official binary) for Ubuntu
# Extract the archive and add Clang to the PATH
RUN apt-get update && apt-get install -y \
  xz-utils \
  build-essential \
  curl \
  && rm -rf /var/lib/apt/lists/* \
  && curl -SL https://github.com/llvm/llvm-project/releases/download/llvmorg-14.0.0/clang+llvm-14.0.0-x86_64-linux-gnu-ubuntu-18.04.tar.xz \
  | tar -xJC . && \
  mv clang+llvm-14.0.0-x86_64-linux-gnu-ubuntu-18.04 clang_10.0.0 && \
  echo 'export PATH=/clang_10.0.0/bin:$PATH' >> ~/.bashrc && \
  echo 'export LD_LIBRARY_PATH=/clang_10.0.0/lib:$LD_LIBRARY_PATH' >> ~/.bashrc

# Install neovim and other necessary deps for use
RUN apt-get update && apt-get install -y python3.6 git locales neovim python3-neovim

Also, at this stage, we can setup some env variables as well for terminal use.

RUN sed -i '/en_US.UTF-8/s/^# //g' /etc/locale.gen && \
    locale-gen
ENV LANG en_US.UTF-8
ENV LANGUAGE en_US:en
ENV LC_ALL en_US.UTF-8

At this stage, your Dockerfile should see something like this.

FROM ubuntu:18.04

# Make sure the image is updated, install some prerequisites,
# Download the latest version of Clang (official binary) for Ubuntu
# Extract the archive and add Clang to the PATH
RUN apt-get update && apt-get install -y \
  xz-utils \
  build-essential \
  curl \
  && rm -rf /var/lib/apt/lists/* \
  && curl -SL https://github.com/llvm/llvm-project/releases/download/llvmorg-14.0.0/clang+llvm-14.0.0-x86_64-linux-gnu-ubuntu-18.04.tar.xz \
  | tar -xJC . && \
  mv clang+llvm-14.0.0-x86_64-linux-gnu-ubuntu-18.04 clang_10.0.0 && \
  echo 'export PATH=/clang_10.0.0/bin:$PATH' >> ~/.bashrc && \
  echo 'export LD_LIBRARY_PATH=/clang_10.0.0/lib:$LD_LIBRARY_PATH' >> ~/.bashrc

RUN apt-get update && apt-get install -y python3.6 git locales

RUN sed -i '/en_US.UTF-8/s/^# //g' /etc/locale.gen && \
    locale-gen
ENV LANG en_US.UTF-8
ENV LANGUAGE en_US:en
ENV LC_ALL en_US.UTF-8

CMD [ "/bin/bash" ]

Now you can build the Dockerfile using the following command.

# Make sure to be in the same directory as the `Dockerfile`.
docker build -t competitive .

Step 3: Mount the directories

We will also require a file system so that files can be shared between the host OS and the Docker container. We want to have a bi-directional communication from the host OS to the Docker Container running.

If something changes in the Docker container, it should also be reflected in the main host file-system and vice-versa.

To perform this, we can use Volumes to attach directories from the host system to the Docker container.

This can be done via the following command, which will run the container from competitive image.

docker run \
    -e TERM=screen-256color \
    -v $(pwd):/app/competitive \
    -v $HOME/.dotfiles/nvim/.config/nvim:/root/.config/nvim \
    --name competitive -dit competitive

Above command will run the image competitive in a container name competitive, and attach the directories from the host operating system to the Docker container.

Step 4: Ready

Now that the setup is almost done, you can exec into the container and start working on the files. You will get the exact libs for work which you require from the comfort of different Host operating system.

docker exec -it competitive bash

Gotcha

When you restart the system, Docker containers will be stopped. You need to explicitly start the container using container id next time.

To solve this repetitive action, I created a small script to find the container with the name competitive and start the container if ti is not already present.

#!/bin/sh

containerId=`docker ps -a -f 'name=competitive' --format "{{.ID}}"`
if [ ! -z "$containerId" ]; then
    echo "Found existing container, Starting it"
    docker start $containerId
else
    echo "Container not found with name competitive, creating one"
    docker run \
        -e TERM=screen-256color \
        -v $(pwd):/app/competitive \
        -v $HOME/.dotfiles/nvim/.config/nvim:/root/.config/nvim \
        --name competitive -dit competitive
fi

echo "Starting container"
docker exec -it competitive bash

In the above snippet, we are first checking if there is any container with the name competitive. If there is, then it fetches the container ID and starts it, else it creates a new container and exec into it.

Final workflow should be something like following:

pde using docker final demo

I had a great time exploring Docker and what we can achieve via some simple scripts.

Prev: Runtime Polymorphism in NodeJS