Have you ever updated a database for one project just to discover that you accidentally broke another? Ensuring the isolation between your local dependencies can save you hours of rolling back a breaking update or resolving intertwined dependencies. In this blog post, I describe how you can manage your local development stacks using Docker containers and asdf.
What are the dangers of using globally installed dependencies?
Before, I’ve used Homebrew to manage my local dependencies, e.g., PostgreSQL database. Many tutorials recommend it as a quick & easy way to install packages on MacOS. One critical issue of this approach is that it does not permit the installation of different versions at the same time. Also, specifying a granular minor version is not straightforward.
I’ve finally decided to ditch using Homebrew for managing development dependencies after the update of the Vim package somehow bumped my local PostgreSQL major version… It resulted in cumbersome data restoration and a couple of wasted hours.
While conducting my Rails performance audits, I sometimes see projects not paying attention to the consistency of their local and production dependency versions. I understand that it’s not always easy to match the minor release of PostgreSQL or Redis ideally. But the mismatch can result in an unexpected downtime if some obscure feature is incompatible between releases.
Let’s discuss how we can improve this setup.
Docker containers for local dependencies
Docker let’s you run the processes for each of your dependencies in a separate container. With so-called Docker Compose you can describe your stack in an easy to understand
yml file. Let’s see a sample configuration for PostgreSQL and Redis, a standard Ruby on Rails setup:
Notice that we don’t dockerize the Rails app itself, only the dependencies. I’m not a proponent of dockerizing the apps for local development because it causes a cumbersome indirection.
As you can see, we can fine-tune the exact versions of our dependencies and their ports. Data is persisted using a Docker volume.
To spin up your stack run
docker compose up command. A new PostgreSQL database will be created during the initial launch based on the configuration from the
.env.development.database file. To stop the processes, you have to run
docker compose stop. Both Redis and PostgreSQL are exposed on their standard ports.
This approach is very flexible. I use it to run the test suit of my rails-pg-extras library against PostgreSQL versions 11, 12, and 13 in a single command. Each PG version is running on a different port. You can check out the repository for implementation details.
I usually don’t keep the
docker-compose.yml file in the repository. A good practice is to keep only a
docker-compose.yml.sample in a version control, so that every developer can decide on the setup of his local environment.
Configure asdf for managing executables
Let’s now discuss how to work with different versions of executable dependencies like Ruby or NodeJS. I’ve been using various version managers, i.e., RVM, NVM, rbenv… Recently, I’ve discovered “the one to rule them all”, namely asdf.
asdf lets you specify executable versions using a simple config file. You can install it by running:
After installing asdf, you have to add plugins for executables that you want to use:
asdf install to install dependencies specified in
Here’s a sample config file for one of my Rails projects:
There’s not much to explain. After entering a folder with this file, all the executables will be switched to their specified versions. You can define default versions using a
.tool-versions file in your home directory. There are dozens of asdf plugins available for different programming languages and command line tools.
Presence of this file does not mean that everyone from your team must start using asdf.
.tool-versions file checked into the repository can work as a single source of truth on which versions should be used, regardless of how you do it.
I hope that the above tips will help you simplify your local development setup. Before I’ve started using this approach, I’ve wasted dozens of hours resolving dependencies hell. I even avoided updates out of the fear of breaking something in my other projects. asdf together with Docker containers makes switching between projects using different tech stacks virtually seamless.