Personal Development Environment using Docker and Neovim


It is not very uncommon that we move to a new development environment and do not see the necessary build tools and config present for our needs. One such case is when I got handed over a Mac and when I tried to compile my competitive programming template, I ran into an issue with bits/stdc++.h not being available. To fix it, I’d have to use awkward workarounds like copying or symlinking the file. Though it got the job done, but it wasn’t an ideal solution.

I’ve noticed many inconsistencies when developing on an environment that isn’t consistent. To work around this, I started using VSCode’s Remote Docker extension, which allow access to edit files in any Docker container from within the comfort of your editor. It’s really convenient!!! As someone who prefers Neovim for daily use, I thought it would be awesome to bring a similar experience to my go-to editor Neovim.

How to achieve this?

Technically, we can execute any shell program inside Docker container, even neovim

Step 1: Select Appropriate Docker Image for Usage

First, select an image for the Docker container - popular options are standard Linux choices like Ubuntu or Alpine. We’ll stick with Ubuntu because it includes everything needed for a normal competitive programming setup.

After we have selected the image, let’s create a Dockerfile. Update the contents of Dockerfile with following content.

# Dockerfile
FROM ubuntu:18.04

Step 2: Install the necessary dependencies

At this point, the Docker container only has default depepndencies that came built-in with the Docker image. For competitive programming, we need some additional dependencies to install like compiler (clang), LSP server (for sweet auto-completion) and neovim (editor of choice).

Append the following commands in the Dockerfile.

#  Dockerfile

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

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

Now let’s build the Dockerfile using the following command. You can execute execute the following on command line. It will build a docker image with tag competitive, extending the base image of Ubuntu. Once this command get’s completed, we can use this image to create Docker containers.

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

Step 3: Mount the directories

Now the Docker image is ready for usage. It’s time to run the container and mount the needed directories. This is needed so that output of program inside the Docker container can be used outside the Docker container a.k.a host machine.

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 a Docker 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 our container is also running, we can exec into the container and start working on the files.

Since this competitive image extends the Ubuntu Docker image, we will essentially get all the libraries that are present in a fresh Ubuntu box installation.

docker exec -it competitive bash

Gotcha

When we restart the system, Docker containers will be stopped. If we need to use the container again, then we need to explicitly start the container using container id next time.

To solve this repetitive action, We can use the following small script to find the container with the name competitive and start the container if it is not already present.

#!/bin/bash

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 Docker container with the name competitive. If we found one, then it fetches the container ID and starts the Docker container, else it creates a new Docker container and exec into it.

I had a great time exploring docker and what we can achieve with it, to improve the Developer Experience.