Developer Environment
06 February 2025

A developer environment is just as important as the application business logic itself. This is especially true when building an app with a team rather than as a solo developer. If the environments differ, the app might work on Developer A's device but not on Developer B's device. Additionally, when a new team member joins, the team leader must onboard them by explaining how the code works and how to run it. This consumes a lot of time, and the team leader must repeat this process for every new member. So, how do we create an environment that works for old developers, new developers, and any other device?

How

The Environment

First, we should look at the environment (the device itself). If the app works on Device A, it must also work on Device B. This concept sounds familiar, right? That’s containerization. If the environment lives inside a container, it will remain consistent across any device.

To achieve this, we can use Dev Containers. How you run Dev Containers depends heavily on your IDE, here is how to run Dev Containers in Visual Studio Code:

  1. You must have Docker installed first.
  2. Install the Dev Containers extension and the WSL extension (for Windows users).
  3. Open the workspace that will live inside the Dev Container.
  4. Open the Command Palette and type Dev Containers: Add Dev Container Configuration Files, then select the option based on the environment you want to create.
  5. Done! You can now open the Dev Container using the Command Palette: Dev Containers: Open Workspace in Container.

You can check the JSON reference for additional information on how to extend your Dev Container.

TIP

Windows users should run Dev Containers inside WSL because of the performance overhead when using the native Windows system.

The Dependencies

Once the environment is consistent, we should move on to managing the dependencies. This does not refer to the app's dependencies, but rather the development tools used alongside it. We should pin specific versions of these tools instead of blindly using the latest tag. This ensures behavior remains consistent over time.

Commonly, we use the package manager built into the programming language (e.g., package.json in the JavaScript ecosystem). However, this approach doesn’t work when a tool is written in a different language than the one our app is written in. In those cases, we can manage those dependencies as follows:

  1. Manual script to install the tool

This is the most bare-bones way to install specific tool versions, but it requires manual implementation, and maintaining it requires more effort.

  1. Using built-in Dev Container features

If you have already set up the environment, you can use Dev Container features. You can use available public features or create your own feature. This only requires manual implementation if you create your own feature or if the available public features are insufficient.

  1. Using Nixpkgs

Nixpkgs are packages intended for use inside NixOS, but you can use them on other operating systems by downloading the package manager only. Nixpkgs is actively maintained and offers a much larger selection of packages compared to Dev Containers features. The downside is that Nixpkgs pins package versions based on the hash commit rather than the package version itself. To solve this, we can use Devbox, which uses Nix under the hood but can pin packages based on the version number itself.

The Scripts

When building an app, we often need to run specific commands for different purposes, such as running or building the app. We should unify these commands into dedicated scripts that are easy to remember. Beyond making things easier, this ensures other team members know how to perform specific tasks. Here is a list of tools that can be used for this purpose:

  1. Scripts inside the package manager (e.g., package.json, devbox.json)
  2. Makefile (for Linux users)
  3. Task
  4. Just
  5. Manual wiring (e.g., .ps1, .sh)

Example

You can check my repository for an example of this setup.

It contains this stack:

  • Devcontainer with a custom image
  • Devbox for package management
  • Direnv for environment variable setup
  • Task for scripting

I am currently running this on a headless Debian server.

On this page