Contained Local Development Environment: An Experiment

June 2019 ยท 4 minute read

VSCode got a neat bit of functionality recently that allows one to develop inside a remote container. I don’t personally use VSCode, but what it looks like is that it allows you to confine tools, per language, within a container and have the editor execute things like language servers, linters, etc without needing them installed locally.

I’m quite fond of this idea. I’ve containerized a few tools that I use regularly but don’t really want to clutter local machines with. It’s an easy way to share my setups across machines.

I recently started a new $DAYJOB and had to copy config files over as a result. This got me thinking: how far can I take the idea of moving tools into containers?

Jess Frazelle did a fairly well-known talk/demonstration of how she uses containers for everything. While I’m not that smart, I seems like it’d be easy enough to set up certain things inside of a set of containers and have it all “just work”.

Getting Started

After deciding to give this a shot, I came up with a goal that seemed achieveable: contain my Go development environment. I write a lot of Go, so stuffing all of that in a container is easy, but what about connecting from vim?

FROM golang:1.12

RUN useradd -m golang
USER golang

RUN go get -v golang.org/x/tools/cmd/gopls

CMD [ "/usr/bin/go" ]

Running this with docker run -it --rm -p 4389:4389 c/golang gopls -listen :4389 gets us gopls running. It’s not enough, though. We need to volume the path that we’re in currently with -v so that it when gopls receives a request, it can actually find the code we’re working on.

vim-go has a g:go_bin_path. While working towards moving away from having all this state on the local machine, I created some scripts that simply call things in the docker container!

Using a temporary directory to $PATH with go as:

#!/bin/bash

pwd=`pwd`
args=""

if [ -t 1 ]; then
	args="-it"
fi

docker run $args --rm -v $pwd:$pwd -w $pwd c/golang go "$@"

I can now run :GoRun or :GoBuild! I can also run go build and end up with an actual binary. Neat-o. The downside here is that we need one of these shell scripts for every single binary we want to run from the container. Extremely annoying, but we’ll ignore this for now.

“Intellisense”

As someone with the memory of a small peanut, intellisense is very helpful. Thankfully, we can still make this work.

vim-go actually has code completion, but the things I write are not limited to Go. I’ve tried a number of language server clients, and the best seems to be coc.nvim. The configuration scheme is just JSON - so no viml necessary.

Most language server integrations just run on the host, but coc.nvim lets us configure a provider over tcp.

For this experiment, I used:

{
	"languageserver": {
		"golang": {
			"filetypes": ["go"],
			"host": "localhost",
			"port": 4389
		}
	}
}

Running it via docker in a script similar to the basic go one gave stdlib autocomplete:

gopls -listen :4389

However, this wasn’t enough for project code, as $GOPATH in the container was not expected as we mounted pwd and vim is asking for data on the path in the host.

After updating the script with -e GOPATH=/local/gopath, package-level autocomplete worked.

Enough… For Now

Obviously, there’s a lot more to do - this isn’t a particularly good setup. Furthermore, performance leaves a lot to be desired. Running this on a Linux host is fine, but testing on Windows and OSX shows just how slow this setup can be.

I’ve seen a few examples of folks putting everything in a container: vim, compilers, language tools… seems like that approach would be a lot better in terms of speed. It’d also simplify a lot of the moving parts - there would be no need for shell scripts that pass commands to docker. I think I may try this out for a time just to see how it goes - it’s not exactly the same as what I originally envisioned but it’d allow me to pull down a single image at the very least.

One idea I toyed with was using docker-compose with vim and docker in a container, a proxy to the local docker instance, and go inside of its own container - allowing the vim instance to call docker commands. Shell scripts would be baked into the container to prevent the need to pull them down to individual machines. Linking them in compose would make container-to-container communication easier as well. Unfortunately this offers nothing in terms of performance.

I’d really rather not have to bake in everything to one giant container. At that point, it might as well be a VM - even if it’d be a bit easier to manage.

I will continue toying with this idea in the next few months - I expect that there will be more to say after researching this some more.