Skip to content

Instantly share code, notes, and snippets.

@edulix

edulix/post.md Secret

Last active March 3, 2023 06:16
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save edulix/76c507c4711c703f76ccebd11b8c1073 to your computer and use it in GitHub Desktop.
Save edulix/76c507c4711c703f76ccebd11b8c1073 to your computer and use it in GitHub Desktop.

A bag of Dev Containers tricks

Nowadays, there are a lot of tools, libraries, IDEs & plugins that are supposed to make the life of developers easier. But this also adds complexity. And worse, it potentially makes the development environment of each developer a bit different, depending on how much did the developer achieve on configuring all these tools.

Dev Containers is yet another one of those tools, but it's one of the most promising projects to solve this issue. Instead of spending time configuring your local environment and fighting against your special local machine configuration and needs, you can crowdsource that.

With devcontainers, anyone can improve the development environment configuration and everyone benefits. 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! And with all this tooling, it has only gotten worse and worse.

Given enough 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 try 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. Follows a bag of tricks and tips of our own, in no particular order:

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 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

Dev Containers use docker to make the development environment reproducible. Which is nice. You can configure the Dev Container can execute a onCreateCommand when the container is created, for example configuring and building your source code and fetching all the dependencies. However, 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. That is NOT good. You don't want to wait 20 mins or 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 cache online service for Nix. We use it in Github Action 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 using crane. And of course we do. 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 do it in the prebuilds to have that already built-in within the Dev Container.

Trick #6: Leverage the power of vscode

You can use all kinds of VS Code stuff within devcontainers, and everyone will benefit from the time each one 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. Just another compounding game-changer.

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. And Sequent 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 in many ways during development. For example, recently I was developing the bulletin-board using devcontainers and I needed, for this feature, 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, devcontainers 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 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 and that just doesn't work.

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. Or.. you get the point.

You can orchestrate the launch of multiple containers with docker compose in devcontainers. Actually, just because you can, it's more flexible to always configure your devcontainer.json using docker compose.

Trick #9: Multiple devcontainer 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 devcontainer 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 devcontainer 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 devcontainer 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 devcontainers and it 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment