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.
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:
- Having a secondary machine with spare resources.
- 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:
- You can configure the editor settings in
.vscode/settings.json
. If your project is using 80 character lines, maybe you want to add a ruler with"editor.rulers": [80],
. This makes the policy clear for the whole development team. - You can setup preinstalled vscode extensions in
.devcontainer/devcontainer.json
. - You can configure your project debugging settings in
.vscode/launch.json
. - You can configure some typical tasks like running the unit tests, running the server backend or applying the linter with
.vscode/tasks.json
.
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"] }
.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:
- If the problem is that Nix is using too much space, try running
nix-collect-garbage
. - You can use the Github Codespaces UI to export changes to a branch.
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.