Everyday Rails

Using containers as a Rails development environment, part 4: Composing services

By Aaron Sumner, March 14, 2021. File under: , .

In my notes on my previous experiment, I demonstrated that SQLite can be integrated into a Rails development container with minimal fuss. However, other common database engines do require some extra effort to integrate. It’s not uncommon to run multiple services in a container-based development environment—for example, a database, a background job processor, and the actual application software being developed.

To make this all work, I’ll need to break out of some of the default settings provided by Visual Studio Code’s Remote-Container extension. So I’ll do that in this article: A relatively quick refactor the configuration to use Docker Compose, so it’ll be ready for additional services in future experiments. This experiment will be on the shorter side, but it will set up more complex additions to the setup going forward.

This post is part of my ongoing series of experiments on seeking developer happiness through Rails, Docker, and Visual Studio Code's Remote-Container feature. You may find background information from earlier posts useful here.

Making the change easy

Right now, .devcontainer/devcontainer.json does a lot of the lifting for our development container. The interesting part for this experiment is the build key:

 	"name": "my-app",
	"build": {
 		"dockerfile": "Dockerfile",
 		"args": { 
 			// Update 'VARIANT' to pick a Ruby version: 2, 2.7, 2.6, 2.5
 			"VARIANT": "2.5",
 			"NODE_VERSION": "lts/*"
	// ...

For more complicated builds, VS Code supports integrating Docker Compose. Here’s what my first pass looks like. Note that it sits inside the .devcontainer directory, to communicate that it’s specific to development environments.

version: '3'

    user: vscode

      context: ..
      dockerfile: .devcontainer/Dockerfile
        # Update 'VARIANT' to pick a Ruby version: 2, 2.7, 2.6, 2.5
        VARIANT: 2.6
        NODE_VERSION: lts/*

      - ..:/workspace

    # don't shut down after the process ends
    command: sleep infinity

This may look familiar if you’ve worked with Docker in the past:

  • The file defines a service called my-app (for illustration purposes only here; name it something that makes sense to you).
  • Actions inside the container will be performed by the vscode user to match VS Code’s defaults.
  • The build context is one directory up from the location of .devcontainer/docker-compose.yml, or the root of the Rails application.
  • Use the Dockerfile that’s already in place for the development container.
  • Apply args that were previously set in devcontainer.json for Ruby and Node versions.
  • Map the local path to the Rails app (..) to the /workspace volume in the container, so edits made in the application code will be reflected within the container.
  • Use command to keep the container running after it’s spun up. My understanding is this is is necessary for Remote-Container to be able to shell into the container via the VS Code terminal. My understanding is fuzzy and could be wrong.

Now, I can replace build in .devcontainer/devcontainer.json to use the new Docker Compose setup:

 	"name": "my-app",
	"dockerComposeFile": "docker-compose.yml",
 	"service": "my-app",
 	"workspaceFolder": "/workspace",
	// ...

Here’s a rundown of the new keys:

  • dockerComposeFile is the file created earlier, the one inside .devcontainer.
  • service is the service that VS Code will shell into. This value needs to match the Rails app’s service name in the Docker Compose file.
  • workspaceFolder tells VS Code and Remote-Container where to mount the Rails application inside the VS Code terminal. The end result will be slightly different than what was in place before (/workspace vs. /workspace/my-app), but seems to work fine.

With this change, I can rebuild the dev container, and aside from workspaceFolder being a little different in my terminal prompt, I’m back in business!


People who work with me know I love Kent Beck’s summation of refactoring: “for each desired change, make the change easy (warning: this may be hard), then make the easy change.”

In this experiment, making the change easy didn’t turn out to be that hard, but I’m hoping it’ll still make the changes (additional functionality in the development container) relatively easy.

And thinking a little longer-term, will this change make breaking out of Visual Studio Code at some point easier? Time will tell. That’s what’s cool about treating all of this as an experiment!

Next steps

Okay, with this change in place, I’m ready to really, really get Postgres or MySQL wired in (or Redis, for that matter), and will start on that next unless I find another yak to shave. Thanks for reading, and see you in the next post.

What do you think? Follow along on on Mastodon, Facebook, or Bluesky to let me know what you think and catch my latest posts. Better yet, subscribe to my newsletter for updates from Everyday Rails, book picks, and other thoughts and ideas that didn't quite fit here.
Buy Me A Coffee

Test with confidence!

If you liked my series on practical advice for adding reliable tests to your Rails apps, check out the expanded ebook version. Lots of additional, exclusive content and a complete sample Rails application.


Ruby on Rails news and tips, and other ideas and surprises from Aaron at Everyday Rails. Delivered to your inbox on no particular set schedule.