SixThirteen

The ephemeral, reproducible dev environment

It baffles me that despite the emphasis on automation and the pursuit of best practices for code and operations, the software engineering community places so little attention to the local development environment. Namely, I am referring to the general complacency towards concern with portability and streamlining when it comes to anything not on the production servers. We should always be striving to minimise the time it takes to set up an environment in order to get things done, whether this be company onboarding or simply to ensure we have manageable and realistic expectations.

Please don’t litter my system with your dependencies.

As commonly discussed a subject as “dependency hell” is, it seems most are happy to view this problem as solved by simply employing version managers, such as venv, nvm, rbenv, asdf, et al. These tools aim to “remedy” the problem by way of messing with your shell via shims so that specific versions of these dependencies are loaded into your $PATH based on the current working directory.

The problem inherent with this approach is that you are now bloating both your filesystem with extraneous versions of entire runtimes and their dependencies as well as impacting your shell environment. Even more oddly, many end up employing these same solutions into their production environments—why does a production server require a version manager?

Not addressed by such version manager solutions are the dependencies outside the language itself, such as the local database being used and the breaking changes that may occur from one version to another, how you choose to manage your environment variables, and more.

It is for this reason that companies often provide their employees laptops—not so much due to the security concern around IP but that it’s perceived as “easier to ensure that everyone has the same setup” in order to actually do the work.

The truth is, there are very simple solutions to get around the problem and we should not be forced into a specific local setup or to use specific IDEs or even operating systems just to get working. We should be empowering developers to use whatever tools make them productive while relying on portable local solutions for everything else.

Tangential to the problems caused by enforcing a brittle dev environment upon all who wish to contribute to a project is the lack of reproducibility ensured by its developers who hesitate in tearing down and rebuilding the project itself out of a fear of breakage. The same is true for thoroughly testing migrations between versions, etc. There should be no such hesitation in the tear-down/rebuild cycle and performing it regularly helps catch bugs that might otherwise go unnoticed. At the very least, this habit serves as a good safety check rather than waiting for the next developer to encounter the problem when merging your changes.

The tools you choose to use to get a job done should not matter nor should they be dictated by the project you are contributing to and this is every bit as true regarding your operating system and local runtime.

A solution: local containers should be for local dev

Despite a tremendous adoption of containerisation thanks to the popularity of Docker (which is not to say it is the only option out there), many projects overlook the benefits that could be gained with containers for fully managing local development environments for teams and individuals alike.

As an example, let’s imagine a project for a client whose needs are simple—only a static site generated using Hugo. The very first step of such a project, according to Hugo’s very own instructions, is to either compile Hugo or rely on your operating system’s package manager to get the project running. However, if someone else on your team already has Hugo on their system, they are now forced to resolve a potential version conflict. Potential for breakage in APIs between versions is considerable. However, perhaps another Hugo project relies on the version already installed on their system. Should you be forced to spend time resolving conflicts upgrading an unrelated project just to begin something new? This is, of course, a rather contrived example but one simple enough, I hope, to highlight the point.

Using docker, however, we could create a multi-stage image that includes an initial setup or base stage that contains the runtime and other needs for the development environment allowing us to simply mount our local working directory as a volume into that container. We now have everything completely isolated. No need to have go, hugo, or anything other than docker running locally.

An example of such a multi-stage image for a Hugo project could be defined as follows:

FROM alpine:latest AS build
RUN apk add --update hugo
WORKDIR /srv/app
ENTRYPOINT ["hugo"]
CMD ["--help"]

N.B., Version-pinning has been omitted for the sake of the example.

You can now run any commands needed as you would do so normally. For example, you might begin by creating a new Hugo site with: docker run --rm -p 1313:1313 --network host -v "$PWD":"$PWD" -w "$PWD" hugo:build new site . --force.