Dev Container CLI Escaping the IDE Restrictions

Dev Container CLI: Escaping the IDE Restrictions

Jake Everhart Development Technologies, Docker, Programming 1 Comment

In past blogs, I have discussed development containers (dev containers) in detail, from explaining their general mechanics to showing how they can bolster a team’s build automation. As a brief recap for the uninitiated: dev containers are a way of encapsulating a developer’s setup into a container, typically a Docker container. As a practical example, rather than forcing a new teammate to manually install and configure all the necessary tooling before contributing to a project, they can leverage a team’s devcontainer.json definition file to quickly spin up a fully configured development environment.

Microsoft has championed this workflow over the past few years, offering tight integration with tools like VS Code and Codespaces to make containerized development as seamless as possible. At the time of writing, the developer experience has reached a point where I honestly prefer to operate within a dev container for certain types of projects. When I open a team’s codebase within VS Code and it informs me that they have provided a dev container to use, I have higher confidence that I’ll be using the same versions of their tools and seeing the behaviors that they expect.

I’ve even come to trust these setups more than an equivalent set of Dockerfiles or docker-compose scripts, just because the simplicity of the ecosystem makes it more likely that everything is well-maintained and configured correctly. It’s easy to see how these standardization and automation benefits can be a huge boost to teams…once they’ve adopted the right tools to integrate with them.

But what if you don’t want to use VS Code?

Dev Containers as a Standard

While dev containers have been popularized by their tight coupling with Microsoft’s IDEs, the Development Container Specification actually exists independently of any tooling. This means nothing stops someone from implementing the same integration with other IDEs, or even exposing the underlying commands through a user’s terminal.

To emphasize this last point, Microsoft has exposed the Dev Container CLI as an open-sourced project to serve as a reference implementation. In theory, this would allow someone to still benefit from a team’s dev container setup without restrictions placed on the developer experience or the editor being used. They could use the Dev Container CLI to execute any build commands that require access to their developer tooling while still directly manipulating the project files in whatever editor environment they prefer.

This blend of automation, standardization, and freedom sounds interesting, assuming that the theory holds true…

Let’s Put This Theory to the Test!

In the Dev Container CLI documentation link above, they recommend using a provided Rust project as an example of the workflow. While nothing is stopping us from using that example for our test, the project is trivial in scope.

To get a better sense of how viable this experience is, we should use something more complex than a Hello World console application. For my test project, I’ll be adhering to the following criteria:

  1. Implement a web server
  2. Include a reasonable example of developer tooling
  3. Use a language that isn’t installed on my machine

Based on these criteria, I’ve decided to create a Ruby on Rails web app. I don’t maintain a Ruby installation on my current machine, and I can satisfy the “developer tooling” requirement by adding live reloading with the hotwire-livereload gem.

That gem also requires a Redis server to be running in the background (which I also don’t have installed), so this seems like a believable use case where someone would realistically consider using a dev container. I already have Docker installed, and I’m willing to install the Dev Container CLI to facilitate this test, but everything else will be done from the terminal.

Requirements

  1. Docker
  2. Web browser (for testing)
  3. Dev Container CLI (installation instructions)

Implementing Our Test

Install the Dev Container CLI

Since I already have NPM and Python installed, I opted to install the CLI through the following command:

npm install -g @devcontainers/cli

Note that other installation options are available within the documentation.

Initiate the Dev Container Session

For this, I created a new rails-example directory to house the project:

mkdir rails-example && cd rails-example

Then, I created a file at .devcontainer/devcontainer.json as per the specification (note the dot in the .devcontainer folder name) and populated it with the following contents:

{
  "name": "Rails example",
  "image": "mcr.microsoft.com/devcontainers/ruby",
  "appPort": [
    "127.0.0.1:3000:3000"
  ],
  "features": {
    "ghcr.io/itsmechlark/features/redis-server:1": {}
  },
  "postCreateCommand": "gem install rails"
}

Before we move further, let’s break down what’s happening here. We’re naming our dev container “Rails example” and using mcr.microsoft.com/devcontainers/ruby as our base image.

This is a Docker image tailor-made to serve as a base “template” for dev containers, and you can find a list of the registered templates here. For our purposes, the main difference between using this image versus a standard Alpine or Ubuntu image is that Ruby has already been pre-installed.

Next, we see the “appPort” section where we’re publishing port 3000 to the host machine. You can read more about the mechanisms behind this here, and I’ll also have more to say about this later in this post. For now, know that this is how we’re allowing our browser (which exists outside of our dev container) to access our web app (which will be hosted at port 3000 within the dev container).

Up next, we have the “features” section. Features are another benefit of the dev container ecosystem, where developers have submitted mutations to be composed on top of a template image. Functionally, this allows us to add whatever tools we want to our container, provided that we can find a feature that includes them.

In a traditional Docker analogy, this is similar to copying and pasting a snippet of Dockerfile or docker-compose lines, which install the tools you care about – just with more encapsulation. In our case, we’re using this to provide our Redis server dependency for our live reloading requirement.

Lastly, we have a “postCreateCommand” defined for installing the Rails gem. This achieves the same result as us manually running the command once we enter the container, but including it here means that a consumer of our devcontainer.json file won’t need to run it themselves.

Once that file is created, we’re ready to start our dev container by running the following CLI command and passing the relative path of our rails-example directory using the --workspace-folder flag:

devcontainer up --workspace-folder .

If all goes well, this will build the image defined by our devcontainer.json file and start the associated container. We can then initiate a shell session within that container with the following command:

devcontainer exec --workspace-folder . /bin/bash

We now have a terminal session giving us direct access to our dev container, as the default vscode user!

Set Up the Rails Project

Since the raw content of our app is unimportant for this test, we can use the following commands to let the Rails CLI handle most of the work for us:

  1. rails new .
  2. bundle add hotwire-livereload --group development
  3. rails livereload:install

After this, we should be ready to start the server by using the following command and then visiting http://127.0.0.1:3000/ in our browser:

rails server -b 0.0.0.0

Note that we need to use -b 0.0.0.0 to bind all IP addresses within our container before our browser will be able to reach our app. If this seems weird to you, especially after we explicitly mentioned the localhost IP earlier in our JSON file, you’re not alone. Depending on when you’re reading this post, one or both of these may no longer be necessary. I’ll have more to say on port/IP mapping when we conclude our test.

Assuming you can reach the Rails startup page, we’re ready to stop the Rails server and test out our live reloading!

Test Live Reloading

This will be easier to test on a new page, so let’s use the Rails CLI again to add one first:

rails generate controller hello index

Now, we can start our server again with the following command, and visit the new page at http://127.0.0.1:3000/hello/index:

rails server -b 0.0.0.0

Now for the moment of truth! Locate the app/views/hello/index.html.erb file within your rails-example folder, and modify the HTML contents with whatever editor you choose. To make good on my promise of only using the terminal, I’ll be using vi to make the change.

As soon as we save the file, we should see our browser update to reflect the new contents:

It works! We now have a containerized development environment defined by the same devcontainer.json specification that our teammates would use, and proof of a usable developer experience!

And to our point, we’ve achieved all of this without booting up VS Code or having to globally install any of the Ruby dependencies. Does this mean that the Dev Container CLI is a suitable way to avoid a containerized team’s editor restrictions?

Well… sort of.

Caveats

I’ll state this caveat outright: this experience is not as polished as what you get within VS Code or Codespaces. Our port and IP address binding issues are both great examples of this.

The forwardPorts property, which is the recommended way of forwarding container ports from a devcontainer.json file, is ignored by the Dev Container CLI at the time of writing. Publishing ports via appPort is a crude workaround that causes Rails to see our traffic as originating from a different IP address.

When contrasted with how VS Code will auto-forward ports exposed by a web server without requiring any explicit devcontainer.json mappings at all, this discrepancy is hard to ignore.

These shortcomings run deeper, as well. According to the Dev Container CLI repository documentation, several other base-level features have not yet been implemented, such as stopping a running dev container without having to directly use Docker commands. A CLI could also never support parts of the devcontainer.json spec that cater to VS Code specifically, like pre-installing certain extensions. Any of these could prove to be major hurdles for a team trying to break out of the constraints of the ecosystem.

Conclusion

Even with these caveats in mind, our experiment was successful. We deliberately threw a non-trivial scenario at a half-finished tool, and it accomplished our goal. It’s also not unreasonable to think that the Dev Container CLI will continue to gain polish over time, or that someone might leverage the exposed standard to create their own tooling in an attempt to rival the VS Code experience.

Even in its current state, if someone preferred using their own editor rather than a team’s standard, I could see this being a viable option for them. At least, as long as they keep an eye out for bugs!

Thanks for reading! If you use the Dev Container CLI, tell me more about your experience in the comments below. I would love to compare notes! And as always, if you enjoyed this post, head to the Keyhole Dev Blog for more.

5 2 votes
Article Rating
Subscribe
Notify of
guest

1 Comment
Oldest
Newest Most Voted
Inline Feedbacks
View all comments