Developing inside a Container, how I use it.

Juan Pablo La Battaglia
7 min readMay 11, 2021

--

I like my console.
I mean: I like MY console, and I think it’s non-negotiable.
And I like to keep things as simple as possible (even if sometimes that makes me work a bit harder at the beginning).

With those two thing in mind, I’ll give you: my version of developing inside a Container.
Enjoy.

Photo by Gear5.8 by Roberto R. on Unsplash

Since I met (and fell in love with) Docker, I always followed closely the new features that allowed to develop inside a docker container.

They added volumes, then they created docker-compose, then they improved docker-compose
But no, none of the alternatives convinced me completely.
Running very long commands to operate on the project (zsh and oh_my_zsh were of great help, but it was not enough).

$ docker-compose exec app rails g migration blah blah blah

Having to change the ownership of the project files created inside docker:

$ chown $USER:$USER -R ./app

And worst of all: having to attach to a running docker to be able to debug.
I’m RoR developer and running pry or byebug inside console is a daily task. Just having to write the following makes me uncomfortable:

docker attach asdf2134

VS Code

As it belongs to Microsoft, I looked at VS Code with some suspicion for a while, but once I got my hands on it and got to play a bit, I buried my old Atom with no regrets.

And after a couple of years of use, and having my configurations synced in the cloud as a gist, I was encouraged to try the “Developing inside a Docker” functionality.
There is another one called “Remote Development using SSH”, with which I was able to do development from a Chromebook, but I will write about that later.

The results of developing inside a container were excellent.

I want to share some thoughts and details that I think make it something applicable to any project.

1) The development environment must have its own separate repository.

In my first attempts, even with docker-compose, I wanted the development environment to be in the same repo as the project code.
But now I see that’s a mistake: there is no point in adding information to a project that doesn’t make the project itself.
Besides, why am I going to force other developers to have something they don’t use, or use differently?
Also, the linking of the environment to the code should be clear and easy to do.
I could achieve that with environment variables.

2) It has to be flexible enough so that each developer who uses it can adjust it to their needs.

One single and statically configured environment can’t satisfy all the devs in a team. It must be configurable enough with just a handful of environment variables.

3) It must maintain the installation status of external libraries and database data.

We do not want to install all the dependencies of our project every time we want to start developing.

I think building a configuration to achieve these points could need a little extra work in the beginning, but once done it has several benefits:

  • The development dependencies are actually documented (let’s face it, who updates the README.md when the framework version is updated? XD) Using this, if it is not updated, we cannot develop.
  • Onboarding new developers is much faster.
  • Re-installing or changing the development computer no longer generates stress by staying unproductive for several hours.

Development environment

Let’s get dirty.

In case you want to check the official docs: https://code.visualstudio.com/docs/remote/containers

Structure

The main idea is to start with and empty git repository and create a directory called .devcontainer and a file inside it called devcontainer.json.
That json file will set some configuration and point to a docker-compose.yml file which is responsible of start the services needed by the main app to run.

As you can see, the structure was designed to be included in a project with code, but as I said, I will treat it as a separate project.

And it looks like this:

First things first, lets create a Docker image with the right dependencies, and some specific configurations to impersonate yourself as user inside the docker. This way you’ll never need for chown $USER:$USER -R again!

The USER_UID is hard coded, because nowadays it is strange to find a linux user with a UID different than 1000. The USERNAME is taken from the your current env variable:

Console

I said it at the beginning: my console is non-negotiable.

In this point, there is VS Code features that fit like a glove.
Like I said I don’t want to force other devs to use my customized environment, so if you don’t want to use zsh it is Ok with this project, with the last version of VS Code, you can select which shell you want:

And there is another great feature for a more detailed configuration. It allows you to use a public repository to download and run an specific script:

I think you can guess, but that repository looks like this:

It has a copy of my .zshrc and .oh_my_zsh and an install.sh script that copy them into the home path.
In case you want to check (and use) my dotenv repo: https://github.com/juanlb/vscode-dotfiles.

Linking to project code.

This is an easy task, we only need to peek inside the docker-compose.yml and .env files:

As the .env file is included on .gitignore, other devs aren’t forced to have the code project inside a particular place. (it can also be configured with a relative path as “../code”, if you don’t want the .env file)

Keys and credentials

Using your id_rsa keys or even your ~/.aws credentials is as easy as mounting those directories as read only volumes on the docker-compose.yml file.

Dependencies

Every language or framework has its own libraries or dependencies that need to be installed in order of run the project.

In the case of Ruby, they are called “Gems”. Gems are installed in the /usr/local/bundle directory.
To keep them installed even if the docker container is destroyed, a new volume is created and mounted to that directory.

I had to change the Dockerfile too in order to grant permissions for the user when they have to install the gems.

Pro tip: in case you don’t know where in file system your dependencies are installed, just run the install command inside the docker, and from another console run: docker diff [container id] .
That command will show you which files were changed, with that info you can add the affected directories as volumes.

Open the environment

With everything in place, the final task is to open your code inside a docker container with all dependent services up and running.

You need to install the Remote - Containers VS Code extension.

and then reopen the project in Container.

The first time the docker images will be built, and if everything is Ok you can open a new zsh console with ctrl + shift + `, and after installing (just for the first time) all project’s dependencies, you’ll be able to start your app from inside VS Code console.

Final words

I tried to keep this post as short as I could, so there are a lot of things I’m not including here (and a lot of things that I don’t even know about this VS Code extension).
But I think that this post is complete enough to guide and motivate you to try it out yourself.

Just as a last tip: I like shortcuts, and I added this one to jump through shell consoles inside VS Code open terminals:

{ 
"key": "ctrl+j",
"command": "workbench.action.terminal.focusNext"
},
{
"key": "ctrl+k",
"command": "workbench.action.terminal.focusPrevious"
}

The repository used for this post is fully functional, and I use it to maintain an old Rails 4 app, in case you want to fork it or use it as guide, I’ll leave the link here: https://github.com/juanlb/vscode-medium-sample

Happy coding!

--

--