Developing from Containers

aghost7 profile image Jonathan Boudreau ・4 min read

Using containers for development has become a widespread practice. The common use case in development is for running services required by the application. Things such as installing Redis, MongoDB, or even Elasticsearch. Most of the time, developers rely on docker-compose to define the whole set of services required for the application.

I've taken things further by creating container images with everything I need to develop in a given language. This includes (but is not limited to):

  • Code editor
  • Debugger
  • Profiler
  • Tracing tools
  • Package manager
  • Build tools


OS Independent

Even if you're using Docker for Mac, you still have a Linux-based environment to develop from. Its really good for getting familiar with Linux, which can then be applied to creating real production container images (among many other things).

Installation Speed

Compared to using installer scripts, container images (in particular docker images) are really quick to get going on a new machine. This is because you only need to download the image with pre-compiled binaries. For example, I've had to compile the cquery server from source for one of my images. If I was using an installer script I'd have to either host the compiled binary myself or compile it on each machine which takes quite some time (since you need to download clang and all that jazz).


If using an installer script, things might not work on a different machine because certain packages were installed in a different manner. For example, someone might've used a PPA to install it one machine, while on another machine it was installed as a .deb archive or using the standard software sources. In contrast, once a container image is built it will just work on practically any machine.


Once the image is built, it won't break out of nowhere. This is unlike installer scripts; I've had many occurrences back when I was just using a git repository to store configurations and scripts where it would break based on when the installer would be run.


Not a Common Use Case for Docker

Support from docker for this sort of workflow hasn't been stellar. Some issues with docker have taken years to fix.

Still Needs Some Glue Code

There are several things which you want to map from the host into the container. I've written a tool to make this easier called slipway. Before I wrote this tool I would use a shell script to initialize the container.

If you're not using Linux as the host operating system, things can get even more complicated.

Anatomy of a Containerized Development Environment

0. Pick a base image

For the uninitiated, building a docker image requires a configuration file (a Dockerfile) which defines steps to be executed in a container. After all of these steps are run, the container is "committed" as a new image.

Most images are based off another image. I recommend starting out with an Ubuntu base since it is very popular.

Add a file named Dockerfile in an empty directory:

FROM ubuntu:bionic

1. Set Up Your User

The image only comes with a root user. We'll need to create a user with appropriate permissions to run inside of the container.

# Feel free to change this to whatever your want

# Create user with passwordless sudo. This is only acceptable as it is a
# private development environment not exposed to the outside world. Do 
# NOT do this on your host machine or otherwise.
RUN apt-get update && \
    apt-get install -y sudo && \
    adduser --disabled-password --gecos '' "$DOCKER_USER" && \
    adduser "$DOCKER_USER" sudo && \
    echo '%sudo ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers && \
    touch /home/$DOCKER_USER/.sudo_as_admin_successful && \
    rm -rf /var/lib/apt/lists/*



2. Basic Tools

Ubuntu core (which is what our image is based on) does not include much since it is built for "ready to ship" applications. Lets add some basic packages for development:

RUN yes | sudo unminimize && \
    sudo apt-get install -y man-db bash-completion build-essential curl openssh-client && \
    sudo rm -rf /var/lib/apt/lists/*

3. Multiplexing (Optional)

If you aren't using a GUI editor like VSCode or Webstorm, you'll probably want a program which can take a single shell session and split it into multiple ones. This is called a terminal multiplexer. I prefer tmux.

RUN sudo apt-get update && \
    sudo apt-get install -y tmux && \
    sudo rm -rf /var/lib/apt/lists/*

There's plenty of customization options which I could discuss here, but to keep the tutorial short I will be skipping over this step. Feel free to check out my project for riced up development environment ideas.

4. Install Your Editor of Choice

Now we need something to edit source code. I can recommend Neovim, but any editor will do.

RUN sudo apt-get update && \
    sudo apt-get install -y neovim && \
    sudo rm -rf var/lib/apt/lists/*

As above, I am skipping customization options.

5. Install Your Runtime

In this example, we'll be installing NodeJs since most developers on the site use it. If you're using Python or some other language at this step you can install it instead.

RUN curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.11/install.sh | bash

RUN . "$NVM_DIR/nvm.sh" && \
    nvm install --lts && \
    nvm alias default stable

Install any additional tools for your language of choice at this step.

Building, then running

All you need is to build the image before you can run it.

In your terminal, run:

docker build -t development-environment .

The final command to run your environment can vary based on what you want to transfer over. Here is an example command:

docker run --rm -ti \
    -v $HOME/workspace:/home/developer/workspace \
    development-environment bash

For a more detailed version of this tutorial, see my repository's tutorial section.

Posted on by:

aghost7 profile

Jonathan Boudreau


Software Developer located in the land of maple syrup.


markdown guide

it's really great to save your space by using alpine based images. usually the most popular images with databases and other tools could have -alpine in tag name. also it's good practice to rely on specific version for the image instead of using :latest for example.

in few months later when you decide to restore your development environment or setup the new laptop it could save our time.


I've tried doing something similar and for development it really doesn't pan out well. You end up trying to build something and you'll be missing a dependency or won't have the needed build tool of installed.

Keep in mind, this isn't for production images, its for developing from containers with your editor and all. If you ask me the usual best practices don't apply, its too different of a use case.


Great post.

I followed your tutorial and now develop from containers and its been pretty easy. Thanks for your work!


A suggestion here is, instead of installing all the required tools, we can directly choose the base platform we are developing for. For example, here we can use Official Node image as base image and install additional tools. over it. Similarly for python or php.


I prefer to use Ubuntu as a base since I'm the most familiar with it. Most of the official images are based on Debian stretch. Also, for NodeJs I really can't live without NVM.


Also, for NodeJs I really can't live without NVM.

Is that because you’re locked into different versions, or does it do something else?

Yea depending on project I have to use specific versions.


Past January 9 (God willing) I will dockerize my production environment.

I install to much stuff on my debian that are there even when not actively working with them.


I assume you find vagrant too heavy weight? This is one of its core use cases against going with containers


Because of the image layering I find docker more convenient if you're going to add / remove things on a more regular basis.


This is nice for TUI development, but hard for full blown IDEs in a GUI, right?

Did anybody do something like this? What are methods for graphical connections? X? RDP? VNC?



It depends what operating system you're using. If on Linux, you can mount the X11 socket and that's pretty much all you need to do. Here's an example:

# allow user inside of container to connect to the X11 server
xhost +si:localuser:root
# create container running gedit
docker run -e DISPLAY -v /tmp/.X11-unix/:/tmp/.X11-unix ubuntu:bionic bash -c 'apt-get update; apt-get install gedit -y ; gedit'

Depending on the program you may also want to expose additional devices to leverage hardware acceleration. For example I think the Oni editor uses webgl.