Nowadays, threre are a lot of tools, libraries, IDEs & plugins that are supposed to make the life of developers easier. But this also adds complexity. Moreover, it may make each developer’s development environment a bit different, depending on how the developer configured these tools.

Blog

A bag of Dev Container Tricks

Technology
    Add a header to begin generating the table of contents

    Nowadays, threre are a lot of tools, libraries, IDEs & plugins that are supposed to make the life of developers easier. But this also adds complexity. Moreover, it may make each developer’s development environment a bit different, depending on how the developer configured these tools.

    Dev Containers are yet another one of those tools, but they’re one of the most promising projects to solve this issue. Rather than configuring your local environment and fighting against your local machine configuration and needs, you can crowdsource them.

    With Dev Containers, any developer can improve the configuration of their development environment. The whole development environment is code: programmable, reproducible, and outsourceable.

    Drifting hidden state

    Traditionally, you have a long-term personal investment in your development environment. Since it’s only for you and you configured it manually, you don’t really want to touch it or invest time on it – it’d be time wasted! As project and tool complexity has increased, it has only gotten worse.

    After some time, you update your Operating System. A new version of Rust is now required for this project and you have to install it. VS Code tells you that there’s a new release, just restart it to apply. The project you are developing now requires newer PostgreSQL installation. You keep being forced to adapt locally to all these changes to maintain a working development environment. And you spend as little as possible on this adaptation, since it’s time wasted.

    Sometimes one of these changes starts giving you headaches. You upgraded PostgreSQL for project A, but then Project B stopped working. Or you upgraded your OS and a library is not found. You get the idea.

    You end up having an ever-changing, undocumented, unreproducible hidden state. You fear the day in which your computer fails and you will have to set up all this again from zero. With no extra benefit, having to spend maybe a whole day just to get things to a state you already had.

    Solve all the above

    Dev Containers is not the only tool that tries to solve the hidden state problem. Nix or virtualenv also try to ameliorate it. But it’s a promising approach because it’s quite comprehensive. More than it looks at first-glance.

    However, like any new technology, Dev Containers have their own peculiarities. What follows is a bag of tricks and tips of our own, in no particular order:

    Trick #1: Remote containers

    Dev Containers run the software in a container – you already knew that. This can consume some more resources (RAM and CPU) and make compilation and other processes slower than just running all those natively in your PC.

    The above is true only if you don’t take advantage of what containers allow. For example, maybe you have a slim or old laptop with little resources, but you have a badass server at home where you can run the containers. You can easily run the docker containers remotely in that server. Suddenly, your computer is a simple thin client with little need for extra resources. You can compile, rebuild, and launch services within VS Code and your laptop’s CPU and RAM usage won’t suffer.

    Trick #2: Github Codespaces

    Trick #1 above is fine but requires:

    1. Having a secondary machine with spare resources.
    2. Configuring this machine to be a remote docker host.

    If you don’t have (1) or if you are just lazy to do (2) like I am, then I’ve got a better alternative for you: Github Codespaces. It allows you to do pretty much the same, except the containers are going to be run automatically by Github in Microsoft Azure cloud. For personal accounts, this includes currently 60 free hours per month, which is not too shabby.

    Trick #3: Prebuilds for Github Codespaces

    You can configure the Dev Container to execute a command with onCreateCommand when the container is created, for example configuring and building your source code and fetching all the dependencies. However, building the docker images and performing from scratch all those steps each time you spin a new Dev container environment can take a while, sometimes even more than 20 minutes or more. That is NOT good. You don’t want to wait half an hour just to start coding!

    Github has you covered here. prebuilds to the rescue. Prebuilds help to speed up the creation of new codespaces by performing these expenses steps and generating a ready-to-use Dev container image when you push changes to your repository. Bottom line is: instead of 30 minutes to spin a new codespace, now it’s maybe a minute and your code is freshly already compiled and ready to go. Feels like magic in comparison.

    Trick #4: nix-devcontainer

    Nix makes builds reproducible and thus safer, so we wanted to use it as a package manager. Unfortunately, some vscode extensions do not integrate well with Nix. To workaround this issue, we use xtruder/nix-devcontainer which applies a hack that fixes it by preloading a given set of extensions, for example arrterian.nix-env-selector, before any other.

    Without this, you would otherwise have to for example install rust toolchain twice: one with nix for your flake, and another via apt-get for VS Code to work properly. Not anymore!

    Trick #5: Leveraging Cachix

    cachix is the most-well known online service cache for Nix. We use it in Github Actions to speed them up and we use it also in the prebuilds mentioned earlier, so that the prebuild process happens faster.

    Within the flake.nix of your package, you can use nixConfig to setup access to your public nix cache for any user to take advantage of, just like we do here:

    				
    					{
      # ...
      nixConfig = {
        extra-substituters = [ "https://sequentech.cachix.org" ];
        extra-trusted-public-keys = [ "sequentech.cachix.org-1:mmoak2RFNZkQjHHpKn/NbsBrznWqvq8COKqaVOI6ahM=" ];
      };
    }
    				
    			

    Now when a user runs nix develop, it will launch the flake’s default devShell but instead of building everything from scratch, it will have read access to the same nix cache as everyone else.

    However, it will be first asked to trust this third-party cache. And this is a nice security feature, but might be annoying for example when running commands within the nix develop environment in the prebuild setup script. To fix this, you can either:

    a) Run any Nix command with an extra --accept-flake-config parameter. 

    b) Configure your Dockerfile to do that by default as we do in Dockerfile and nix.conf.

    Another way to leverage Cachix in Rust projects is to use crane. The beauty of crane is that it allows you to build your rust dependencies just once and then lint, build, and test changes to your project without slowing down. This is something more related to Github Actions, but you might also take advantage of this in the prebuild process within the Dev Container.

    Trick #6: Leverage the power of vscode

    You can use all kinds of VS Code stuff within Dev Containers, and everyone will benefit from the time each other spends in having a top-notch development environment configuration. It’s multiplicative. Here are some examples:

    As we said earlier: anyone can improve the development environment configuration and everyone benefits. 

    Trick #7: Going multi-repo

    Google famously uses a single monorepo architecture. However, in open source typically you don’t. Typically you have multiple repositories to make it easy to let other people collaborate and reuse specific projects. Sequent Voting Platform is open source not only by license but we also buy the philosophy of collaboration, so we are multi-repo.

    However, it can be challenging to manage multiple repositories during development. For example, recently I was developing the bulletin-board using Dev Containers and I needed, for this feature I was coding, to also apply some minor code changes to one of the dependencies of the bulletin-board, strand.

    Should I spin two different codespaces for that? What if I need to touch code in multiple dependencies? Well, don’t worry too much because yet again, Dev Containers and codespaces have a solution for that.

    First, you can configure the devcontainer.json to give git commit permissions to other repositories of the same organizations like we do here. More details in the documentation.

    Second, you can modify your onCreateCommand script to download this and any other dependency locally (just do a git clone).

    Third, use this local dependency. How to do this will depend on your toolchain. If you are using Rust, my advice is: don’t touch Cargo.toml. Yes, one quick and dirty option is to change your dependency from something like, maybe:

    				
    					strand = { git = "https://github.com/sequentech/strand", features= ["rayon"] }
    
    				
    			

    to:

    				
    					strand = { path="./strand", features=["rayon"] }
    
    				
    			

    But then you might end up committing that change in  Cargo.toml.and that just isn’t good ™.

    Instead, you should create a new file to override dependencies called .cargo/config.toml, and add there something like:

    				
    					[patch.'https://github.com/sequentech/strand']
    strand = { path = "strand", features= ["rayon"] }
    
    				
    			
    Additionally, add the .cargo/config.toml to .gitignore to ensure you don’t inadvertently commit this file.

    Trick #8: Use multiple containers with docker compose

    Maybe you are developing a backend service and you need to use a PostgreSQL database to run it. Or maybe you want to be able to run both the frontend and the backend within your development environment. Or.. you get the point.

    You can orchestrate the launch of multiple containers with docker compose. Because why not, it’s more flexible to always configure your devcontainer.json using docker compose.

    Trick #9: Multiple Dev Container configurations

    Contemplate these cases:

    • There are times you need to work with a local copy of dependencies, there are others you don’t.
    • There are some times where you broke your prebuilds and you want to launch a new Dev Container with no setup script.
    • Maybe sometimes you want to develop with an environment using PostgreSQL as a database backend and others with MariaDB.
    • Or maybe you actually have multiple projects within a single repository and you want to be able to have a ready-to-go Dev container for each of them (Hello there monorepo people!).

    All this can be solved using multiple Dev Container configurations. You can have multiple, ready-to-go devcontainer.json files inside the .devcontainer directory, using the pattern .devcontainer/{name}/devcontainer.json. And Codespaces also supports this feature natively.

    Remember these tricks are composable. For example, in this case you can configure prebuilds for each Dev Container configuration.

    Trick #10: Custom codespaces

    In Github Codespaces you can use the Advance Create feature to configure in more detail your new codespace: choose the specific branch, the number of cores or amount of RAM of the container, the Dev Container file, and actually it has a nice interface to just modify manually the devcontainer.json before launching. This can be helpful in disaster recovery scenarios, for example in broken configurations you can edit the onCreateCommand or anything else.

    Trick #11: Garbage collection

    Dev containers are typically launched with a specific disk size. Sometimes this turns out not to be enough. Now imagine you have uncommitted/unpushed changes in the container. There are multiple things you can do:

    Oh and now that we are talking about garbage collection: you can also review and manage all the codespaces you personally have in github.com/codespaces. When working with multiple repository, with multiple features or branches, you might forget about some codespaces.

    Codespaces typically auto-stop after idling for 30 minutes – and of course this is configurable. But they are still wasting/spending disk space. So go to github.com/codespaces and delete all your unneeded codespaces.

    Trick #12: Codespaces vscode plugin

    So you can go to your repo in github.com, click on the big green button (Code) and launch a new codespace right there and it will open the codespace in vscode running within the web browser in a new tab.

    But it doesn’t stop there. You can perhaps close that tab, and then click again in that green button, see there listed your just-created codespace, click on the ... button -> Open in.. -> Open in Visual Studio Code. And if your local vscode installation has the Codespaces extension it will just open in a new window of your vscode.

    You can even forget altogether about the web browser and do the whole thing from within vscode. With Cmd + Shift + P search for Codespaces and from there you can: connect to a codespace, stop a codespace, rebuild it, create a new one from a specific repository.. you name it.

    Wrapping up

    There are other avenues to explore in the future. For example, denvenv.sh also supports integration with Dev Containers and they surely also integrates well with cachix since it comes from the same developer.

    Another trick we have not explored yet is to use Dev Container Features to package (quote) “self-contained, shareable units of installation code and development container configuration”.

    We’ll continue our road to making development easier and keep you updated.