Conf42 Golang 2024 - Online

Using Nix to create reproducible Golang development environments

Video size:


In this talk, I will show how you can leverage the powerful Nix package manager to create consistent development environments, for Golang projects. Allowing for superfast setup for onboarding new developers into projects. Then also how we can have more consistent development and CI environments.


  • Haseeb Marjid talks about reproducible and ephemeral development environments with Nix for Golang projects. Nix is a declarative package manager, powered by Nixlang. Talk aimed at those looking to improve developer experience, particularly around consistency of development environments.
  • NICS is an ecosystem of tools. We want to have reproducible and ephemeral environments for developers. Current packaging systems all have various flaws. The upsides beat the downsides of Nics, in my opinion.
  • With go, we need tooling to aid development. How are we going to create this Nix environment? Don't worry about the syntax of nics. And we want to make sure certain tooling is available in that. Nix can come in and fix a few of our problems.
  • Nix flakes can output a lot of stuff. They can output build a docker image they can build an ISO, they can create a development shell, which is what we're going to do here. We can leverage flakes and dev shells for installing packages.
  • One thing that can really improve the developer experience with NICs is pre commit. With these pre commit hooks you can get some again really fast feedback. Saves us time waiting for CI and save some credits as well.
  • Everything in Nics is an expression which is quite common for functional programming languages. When we're building packages, what's actually happening? It's a two step process. The derivation is built into a package, and that is what has a side effect on our machine where stuff is actually getting installed.
  • Nix flakes basically take state on our system and kind of put them into code. With this flake lock file, we lock our stuff to specific revisions. flakes improve reproducibility across our system by locking our dependencies. But do note they are an experimental feature.
  • One of the final topics we're going to touch on is CI. We want to leverage Nix's cacheability and sharing dependencies. So it shares dependencies between different stages and different jobs. And this is kind of what it looks like, eleven and a half minutes total runtime.
  • Nick O'Brien talks about Docker in terms of like as a development environment. Give both a go, see which one you prefer. But I think I found nics a lot easier. A few things you can look at in your own time to further use nics.


This transcript was autogenerated. To make changes, submit a PR.
Hello everyone, welcome to my talk about reproducible and ephemeral development environments with Nix for our Golang projects. A little bit about myself my name is Haseeb Marjid. I'm a backend software engineer at Fintech called Curve. There's a link to my blog, a few fun facts about myself, very like cats, and I'm an avid village cricketer, bold and underlined on the village part there. But I'm looking forward to the cricket seasons dying in the next couple of weeks or so. It'll be good to get outside. So who is this talk aimed at? It's aimed at few groups of people, the first group being those who are kind of interested in Knicks and want to learn a bit more. And for sure we'll cover that. It's also aimed at those people who are looking to improve the developer experience, particularly around the consistency of the development environments. So things like going back to an old project like six months ago that you haven't touched and you're worried that you're going to spend a half a day yak shaving getting the development environment working, you're going to want to want to faff around with that. You want to just get on building features, bug fixes, adding tests, whatever you want to do, work on actual code. And so we'll take a look at how we can use nics to help with that. We'll also look at how we can make it easier to onboard new developers. So they have to type one or two commands just to get set up on a project. And especially if we can have consistency across lots of our projects, imagine at work then it means that developers kind of know they have one or two commands to write and they can jump between projects really easily and again get coding as fast as possible. Then also we don't just want things to work on our machine, we want them to work across everywhere we run this. So whether that be locally CI or even say like your various environments that you have production dev, etcetera, etcetera. And we don't want to annoy Samuel Jackson, do we want it to work everywhere? We don't want it to just work on our machine. So some of you probably wondering what's Nix? Nix is a declarative package manager, and we'll cover what declarative means in a second. But package manager in the sense it's a tool for installing packages on our machine, which is kind of, especially as a software developer, kind of a fundamental thing we need to do on our machines is install packages. It's similar to tools like ApT, Pac man or brew that you may be familiar with. It's powered by this thing that I'm going to call Nixlang. You may hear it called the Nix programming language and it's this pure, functional and lazily evaluated language. And what we mean by pure and functional is that it doesn't have really side effects. It does have like one or two, but basically for the same input, has the same output. It doesn't really depend on the state of your machine. So it means if we have these like NICs configuration files, we can kind of easily move them between devices and people because, you know, it's not really relying on the state of your machine, which is quite nice and lazily evaluated in the sense that it just works out kind of the bare minimum it needs. It's lazy, it's just like, okay, I have enough information to go build this package, I don't need any more. And that's particularly useful. You know, we have 80,000 packages in the Nix packages repository. We don't want to build all of them, for example. We just want to build the ones that we need for the packages we want, right? There's also this thing called Nixos Linux distribution that's powered by Nixlang and can be configured using Nixlang. And it's also powered by the Nix package manager. We're not really going to talk much about that. I do daily drive it, it's pretty cool. But just know it's separate from Nix. You could have like say an Ubuntu machine that's running the Nix package manager. So when we say something is declarative, what we mean is that we kind of just care about the final state of things. Whereas typically package managers are imperative where we're giving it like step by step instructions. So for example, if I was to say make me a cup of tea and then I gave you instructions like, you know, get the teabag, turn on the kettle, etc. Etcetera, that would be what you might classify as imperative. Whereas declarative is just me saying can you make me a cup of tea with milk and sugar? And then you kind of work out how to get to that final state. And it's very much the same with nics. In this case we have the sway tiling window manager that we want to turn on and we want to turn off the I three window manager. We're not telling Nix how to do that. We just say this is the final state and mix goes off, runs off and does that. One of the other cool things about generally when things are declarative is we often put them into code and then we get other benefits, like version control, easier for people to review, easier to reproduce as well, because again, it's not really caring about the state of our system. If I don't have I three installed, for example, Nix will just work this out fine, it just won't uninstall I three. It's pretty cool. Then I think, I think it's definitely a really useful feature of Nix. And then once I definitely will have converted you to Nix, you can start busting this out into conversation. By the way, I use Nix to your friends and just doesn't matter what the conversations, you can always change it and I'm sure they won't mind at all. What's the problem we're solving? Well, imagine like typically when we have binaries installed, we might have them in user local bin, like go Lang ci lint. There are a few problems that the typical package managers have. It's like, what dependencies does this need? Like runtime and a build time? Like how easy is it to discover that? What configuration flags was this tool built with an environment? Variables, right? If you wanted to like build the same version yourself, how'd you do that? And then how do we have two versions of this package? What if we need to have version two, for example, in version one? Now as far as I'm aware, Go Lang Ci lint doesn't have a version two, but it might. And for some projects, you might want to use version until you've upgraded it. And for some projects, you might want to use version two. How do you do that? Because typically our package managers replace the binaries in place, right? So we replace this with version two. So how do we maintain multiple versions? And there's various packaging solutions that kind of solve some of these problems. You have like snaps and flat packs which create these sandbox environments, and they have their dependencies, I believe, in these kind of sandbox environments, and they don't really interact with the rest of the system. We have ASDF for managing like multiple versions of some of our tooling, like Go node, Python, et cetera, et cetera. We have virtual environments and to a certain degree go modules, so we can have per project dependencies and we don't need them globally installed. More so for virtual environments than go modules, but yeah, virtual environments used in python. For those of you who may not know, to summarize this section, we want to have reproducible and ephemeral environments. NICS is an ecosystem of tools. So we have NICs, Nixos, Nix packages, the Nix programming language, the main thing, of course, being the package manager. And then our current packaging systems all have various flaws. There's nothing in software engineering that's a silver bullet, Nics included. It can be a bit complicated. Nics, you kind of have to learn this programming language, which puts people off. But I think the upsides beat the downsides of Nics, in my opinion. So if we take a look at this demo, we look for this go lang ci lint binary, can't find it. We go into this project, we load this dev environment, and then when we look for this binary, we can find it. And when we leave this project, we will no longer have this. And this is kind of the state we want to get to. This is kind of what we want for our developers. And we want to make sure that people are getting the same versions of tooling as everyone else. In this case, I think it's version 1.56.2. We want to make sure all the developers are getting that same versioning. And when I say ephemeral development environments, what I mean is short lived, temporary in the sense that just existing for the lifecycle of this project. And when we leave it, the environment's kind of gone or not loaded. But yeah, so that's kind of what we're going to achieve and we're going to take a look throughout this talk how we can achieve that. So how does this relate to go lang? Well, with go, we need tooling to aid development, right? Like we might need binaries to generate code coverage reports. We may have tools to vet our code. Static code analysis, container code analysis, all these things that go into development, you know, Docker and Docker compose. We have, you know, dependencies for our projects and we're not really going to worry about those, you know, that we manage viago, but the other tooling we need to aid our development, maybe we have a task runner, like we have makefiles, right? Like we might do make lint or make test. And we want to make sure developers have similar versions or the same versions if we can. And we want to make sure the same versions are running in CI, they're running locally, because I'm sure we've all been bit by the bug where it fails in CI is working locally and it's just because of a version mismatch. Couldn't see our word for a second. So one way we can kind of do this is, and we do the set curve for some of our projects is we have this tools go file. And essentially what we're going to do here is we're going to manage our dependencies using go module. So we install these various packages using go modules and then we add these underscores here to trick go modules into thinking it's important. So if you do a go mod tidy, it doesn't remove these. And the cool thing is then they're all kind of managed Vigo modules and we can do an update. We just update one file and it will update our dependencies and etc etcetera. It's quite nice. And we have this build flag so it doesn't get built with our binary and that works for go dependencies. And we maybe have a make target like this which installs these dependencies in our go path bin folder, which is not which works. But then we encounter similar problems as what we're talking about before. What if between projects we have version one of one tool and version two of another tool. We kind of have to remember then to run this tool which is going to overwrite the binary in that go path bin folder. There's something else we have to remember to do when we're jumping between projects. And then what if we want to manage tooling not related to go? What if we wanted to make sure that the user has GNU make or GNU parallels or some other CLI tools and maybe we have some bash scripts or something. And we want to make sure certain tooling is available in that. And so I think this is where Nix can come in and fix a few of our problems. So let's take a look at how we can create a development environment in Nixon. So imagine we have a project like this, really simple go project. How are we going to create this Nix environment? Don't worry about the syntax of nics and what's kind of happening behind the scenes. We're going to take a look at that just later in this talk in a couple of minutes. But first we're going to take a look at how we can create this development environment. So we have this flake Nix file. Think of it as a main go file, as the entry point to our Nix configuration. It has a bunch of inputs and a bunch of outputs. In this case, our inputs are all basically going to be git repositories. So one of them being Nix packages, which is this repository that has 80,000 packages. So this is where we're going to install our packages from, has a bunch of outputs. Nix flakes can output a lot of stuff. They can output build a docker image they can build an ISO, they can create a development shell, which is what we're going to do, Dev Shell. They can build a package, they can do lots of various different things, but we're really just going to focus on dev shells today. But it's just good to know generally speaking. So we have this helper library called flake utils, which basically just reduces some boilerplate in our flake where obviously packages have to be built for specific architecture. So you know, like x 86 64 Linux or Arch Darwin, you know, it's like AMD and intel versus arm based chips, different architecture. So we have to build the binaries differently and so packages are built, we have different slight packages. So we're here basically what we're doing is we're just specifying that we want to just get the packages for our system architecture and flake utils. Lib is this library that helps us reduce the boilerplate to do that. But that's basically all we're doing here. So don't necessarily worry about that here. What we're doing is we're creating a default dev shell. We have this packages make shell function. So between the kind of curly braces is this function and we're passing a parameter called packages, and these are all the packages we want to make available to the user of this dev shell. The details again don't matter too much that I just took that from a project I had called Optinx, which I've linked later on. You can take a look at that. So we take a look at, so how do we use this? Well, if we kind of look for the binary, we won't find it, it's fine. Then we do nixdevelop, which will load our development shell that we just created there. And then when we look for our go lang ci link binary, we can kind of see at this funny path next door, some funny characters hash maybe, and then yeah, cool. So we've created a development environment. So if we kind of summarize what we've done so far, well, we can leverage flakes and dev shells for installing packages. We can load into those shells or that shell using nixdevelop. We can make sure each developer gets the same package. We have this concept of this flake lock file which locks our inputs, and we'll take a look at the syntax of that and how that works a little bit later in this talk, but just keep that in mind and we can update this lock file, but we have to kind of manually do it. So one other thing that's quite cool is we can use this again nix agnostic tool called diranv. And with Diram what we do is create an MVC file, and what we put in this MVC file will get executed when we load into this directory automatically. So what we can do here is do this useflake, which is kind of this helper function for running Nix developer automatically. The very first time we load into a directory that has deriv, we do have to has this MVC file with Dirham, we do have to approve it so we don't just run arbitrary code on our machine. So let's take a look at what that looks like. So imagine I'm at work and I need to add a feature to a project. I clone this example project, I try and find this linter. I want to lint the code right, can't find it? Fine, I load in. I do Durin Valalau, because remember that first time we have to do Durin Valalau which has this useflake. It will load in this dev shell. In this case it's ready cached. It doesn't need to do anything. It can just load in one that I already had. Then in my case, because I'm using starship prompt, and I'm sure other prompts do this as well, it will let me know that I'm in a development environment here with the viya, and it has that little flake, which is quite nice. Just a good reminder that you're in this dev shell. Then when we look for this binary, we can find it. When we leave this folder, we can't find anymore. And so that's again that ephemeral nature I was talking about. And one of the cool things about Dirham is the first time you go into a directory, it'll tell you you have to do Durham Valao, so it's not something that you have to remember, it will tell you. And again, you only have to do it once. Next time I go to that directory, you won't have to run that again. One other thing we can do with Durham, the Nix flakes, is we can point to a dev shell or some Nix configuration that has a dev shell in a remote repository. In this case it's on GitHub, and we can use that if we want. So we can share configuration, and we could use multiple flakes as well if we wanted to. But we're not really going to get into that for this talk. But just to know if you want to have a remote development environment, you can as well, or the config remote, you can use that. One other thing that I think can really improve the developer experience and something we can manage with NICs is pre commit. So you know, we have these things called githooks, which is these scripts we can run at various stages of the git process, like pre commit, post commit, pre push, post push, etc. Etcetera. Then there's this tool which can be a bit confusing, as in this bit confusingly named called pre commit, which will basically help us create these pre commit git hooks for us. So we can create this using nics. So if we do pre commit hooks, add that as input. Again, you can name these inputs wherever you want, just helps to kind of make them somewhat related. We add that into our output section here and say we want to create these pre commit hooks. It has some built in hooks for go that we can use. So we use golangs. We're going to enable the linter and we're going to enable tests. The cool thing is it will only lint and run tests on the files that have changed, as in the ones we're trying to commit. And so yeah, we get some really fast feedback when these run. Saves us time waiting for CI and save some credits as well, build time that could go used for somewhere else. Then we can add the to our make shell function that we had. And the shell hook is just a command that will run automatically. When we do nix develop, I will load into the shell, which is going to happen automatically when we're using Diran. So essentially when we go into the folder it's going to install our pre commit hooks for us. Whereas normally the developer would have to remember to run the pre commit install like it's another command they have to do and now they don't have to think about, which I think is pretty cool. And with these pre commit hooks you can get some again really fast feedback. So it closes that kind of feedback loop and lets the developer know something's going wrong or not. To kind of summarize this bit, what we've done, we can use Durham to further reduce cognitive load on our developers. We can use flakes from remote git repositories, share them between multiple projects if we want. We can also manage precommit in nics. Just something to note that pre commit is usually managed using a YAML file, and now we're using a nIcs. And some people do have an issue with abstracting away from the original the way we configure a tool. I don't mind it, but just something to consider. It's kind of the next section I want to cover is how does Nix work? Like what's happening behind the, behind the scenes. So everything in Nics is an expression which I believe is quite common for functional programming languages. Remember, this is powered by Nixlang. And so what we have is we have a file, maybe it's called shell Nix, and this will get imported somewhere. And we have this function essentially here in this file that takes in one parameter called packages, and then we have these triple dots which ignore any other parameters passed. Then we have this function call called packages make shell, and we pass a bunch of packages we want to install. So in this case, this nix expression, we return one nics expression from the file, which can be a compound of other Nics expressions put together, but we always return one. So in this case we're returning this function, and again this, this file will get imported and during the import that you'll have to pass packages. And yeah, we were kind of doing this with our flake dot nix file, but it was a little bit more hidden, I guess, what was going on. But that's kind of what nix is and kind of what we're doing here. So you might be wondering like, okay, we're, what's this go Lang ci lint? I get it's coming from Nics packages, but what, what does that mean? Well, on nix packages, the GitHub repository, we can go find the go Lang ci Linux expression, and it has this function called build gomodule, which is a helper function for building go modules. And you can see as like a name a version where to fetch it from. GitHub has a bunch of other information about how to build it and if it has any dependencies, et cetera, et cetera. So we have this nix expression there. Cool. If we dive a bit deeper and look at what's behind the build go module, it abstracts away the standard env derivation, where this derivation function is the most important built in Nics function. So when we're building packages, what's actually happening? It's a two step process. So when we do nix develop behind the scenes it'll be calling like Nix build of some kind. And behind the scenes we'll be doing this, this in two steps. And you'll see why we do this in two steps. So the first step is evaluation time. We take the nix expressions and the Golang Ci lint expression, and we return a derivation set. This DRV file where a derivation set or derivation is just a set of instructions how to build a package, kind of like a recipe. Then we have this build time. The derivation is built into a package, and that is what has a side effect on our machine where stuff is actually getting installed. So let's take a look at derivation. So derivations are put into our next store folder. They have the format hash name version dRv, where a hash is a cryptographic hash of all the inputs to that derivation. So let's say we have this go 121 eight derivation, even if we're building go 121 eight, let's say we change an environment variable. We are going to get a different cryptographic hash there, and so we're going to get a different derivation, and for all intents and purposes it's a different package as far as Nix is concerned. So derivations and also packages are mutable. I mean, you can obviously go change them if you wanted to. Nix discourages you from doing that. You probably shouldn't, but you can if you really want to. Let's take a look at what this derivation looks like, and we can run this command at the top there. Don't worry if some of this is cut off. Doesn't really matter, it's more just high level what's going on. So we have a builder, how we're going to build it bash we have a bunch of environment variables. These are the only environment variables made available during the build. We have a bunch of input derivations, so other derivations this depends on, and Nix will make sure these derivations are built into packages before then we have a bunch of metadata where this package is going to get installed, what system it's for, name of the package. Then we have this package, which again is immutable. So if we wanted a different version of this package, let's say environment variable changes, it would be at a different path. And you can think of the path as a unique identifier, as far as Nix is concerned, of a package. Then there's some sim linking done later, which will determine which binary we end up using. Even if you think they're basically the same. Then within Nixdor we have everything we need. We have the binary, we have Sharego and has a bunch of other stuff, which is quite nice because our packages are immutable and we can kind of pre compute them. So Nix has a bunch of servers available to it that are kind of pre building these binaries and packages. And so what that means if a derivation says we need to build a package at this path, we can check if that package exists in our path. If it doesn't. We can go fetch from various different caches. In this case I'm fetching from the official Nixos cache. And the cool thing about that is often because these are being pre built and you can pre build them yourself as well if you want, and you can pre build your own packages if you wanted. It just means we just have to fetch them and download them. We don't actually have to build them ourselves on our machine, which is really cool. So often we're just downloading stuff we're not actually building because lots of these packages are pre built. Another advantage that this kind of approach gives us is that the dependency tree is explicit and we can see what go depends on and then what those tools depend on. So like it depends on bash, and then bash depends on glib C. And every anyone with this exact same unique identifiers and you know, starts with k, seven, ch, et cetera, et cetera will have the exact same dependencies as us if we print out this dependency tree and nextore basically becomes this kind of graph database of our dependencies. One other thing that doesn't matter so much for dev, specifically for dev environments, but it's kind of useful to know because of this approach of we're not updating stuff in place. We can kind of have this concept of generations and profiles and we can roll back to earlier versions of generations. We can also then have atomic updates if we want. Sorry, we do have atomic updates, and that basically means that if something fails during the update we can go back to an older version. Or we don't even end up updating the NICs profile at all because these binaries just end up getting mapped to stuff in Nick store. And yeah, unless we garbage collecting cleanup stuff it's kind of going to be there. One other thing to note is you'll notice that we use an epoch time of one, that is 1 January 1970. The reason for that is because the timestamp date time can be a form of non determinism, because sometimes that gets injected into the binary. What we want to make sure is that we build the exact same binary. Otherwise every time you built a package you get a different binary and so wouldn't, you know, we don't want that. So we set this to 1970 and we make it a deterministic timestamp. Then in the Nix world this is what bincat looks like, not user bincat, but in NICs store. Just to kind of summarize, NICs derivations allow us to have immutable packages, require us to make our dependencies explicit. One thing to kind of note is if a package is not nix packages, you will likely have to package it yourself. But because Nix packages has 80,000 other packages, often or always, I found if I need to package something, it's just that one thing. The dependencies it needs is almost always in nix packages itself. The next bit we're going to cover is nix flakes. So how does Nix flakes relate to stuff? So nixflakes basically take state on our system and kind of put them into code in the sense that we have these things called nix channels which would refer to what version of nics packages were pointing to. And what we do is we just take that and make that kind of more explicit and we'll see exactly what that looks like. With this flake lock file, we lock our stuff to specific revisions, and that means other, because we can put that lock file in code, other developers can then point to the same inputs. We can also use other git repositories, non nix related, and manage them using our flake nix file if we want. They also define some basic structure, because now our flake Nix file becomes the entry point for our Nix configuration. So you know, that's kind of the first file you can go to to look for that Nix configuration. So as we said before, we have a bunch of inputs, a bunch of outputs, the main input being nix packages, or the default input we get when we initialize our flake. Then we also generate this flake lock file, which has this kind of concept. So this is the lock for the next packages input. We have this null hash, which is just a hash of the contents of that input. Less important because it's a git repository, but we could have non git related inputs. In this case, we also have a revision. So anyone using this flake Nixonflake lock file combination will be locked to this revision until we update that lock file. And that means if we're using this in CI or other developers using this will be point to the same version of Nix packages, which is really cool. We can also use like say a GitHub action or CI to update this flake lock file for us if we want to think a bit like dependable style stuff, if you want to do that. To kind of summarize nics, flakes improve reproducibility across our system by locking our dependencies. They provide a more standard way to configure our system. But do note they are an experimental feature, nics, and they could break in terms of like they could be breaking changes, so just keep that in mind. But I think they're great and I use them in all my projects and my own Nix configuration. So kind of one of the final topics we're going to touch on is CI. We've spoken about we want consistent environments between what's running locally and what's running in CI. How can we do that? And definitely we want to kind of leverage Nix's cacheability and sharing dependencies. GitHub Actions has some great stuff, especially from I think determinant systems for leveraging caches and various things to speed up your pipeline. I use GitLab CI and I found this great project by this user called Cynerd and it has this next job. And what it does by default is leverages the GitLab cache, but we could use a cache from an SSh machine as well, or ssh to a machine and copy from there. But essentially what it does is before and after the job, it will just copy from the cache to the next door of the job and then from the next door to the cache. So it shares dependencies between different stages and different jobs. So in our GitLab Ci file we include this GitLab Ci file, the GitLab project we just saw. Then I have a stage called pre that extends this nix job and we just do nix develop to install our dependencies. Then at future stage dependencies have been cached, they'll be copied over to this job and we can do nix develop C Go Lang ci lint run, whereas normally we'd be in that NaICs development environment and we could just do Golang Cilantro and we should be running the exact same versions because of the nature of Flake Nix and the various things we've talked about with Nix. So that's really cool. And this is kind of what it looks like, eleven and a half minutes total runtime. I think before I did this change it was 22 minutes, rather anecdotal, but something to note. Kind of just taking a look at Ci logs again, we can see it's copying from that cache, which is really nice as well. And this was definitely me with Nix when I first heard about it, I didn't really get it, and then eventually it clicked and I think it's really, really cool. And hopefully you guys either think that at the end of this talk, which means I've done an amazing job. Unlikely, but I'd recommend just giving it a go and seeing what you think. But before we close out this talk, I'm sure some of you have been thinking, or maybe even screaming at your computer, why not Docker? And I definitely was confused, like how does this relate to Docker? So remember, we're talking specifically for Docker in terms of like as a development environment. I think Docker is still great for building and packaging and deploying our stuff, especially on the cloud. And specifically what I'm talking about is Docker files to build docker images. And one of the problems Docker has, I think by being imperative, you'll often hear the term it's repeatable, not reproducible, often because of the package managers we're using. But imagine you have a Docker file. If two people try and build it like six months apart, you're probably going to get a different docker image and therefore it's not reproducible. One other problem I have with, say, Docker dev containers, which versus code plugin, which I think now works with jetbrains as well to try and make it easier to develop within a container, is what if I have specific tooling that I want, I now need to make that available, like say fzf z oxide. I have a specific shell, I have to make that available in the Docker image, and I'm potentially bloating that Docker image as well. I did find a way to personalize it. You can kind of run this startup script, but that was a bit slow and a bit more cumbersome than I found comparing it with nics dev containers. Sorry, Nick's dev shells. So just something to note there. Give both a go, see which one you prefer. But I think I found nics a lot easier. A few things you can look at in your own time to further use nics even more like nics. All the things here's a link to my slides. I'm going to have a bunch of other links, just go through them in your own time. Just a bunch of lots of literature and articles and YouTube videos. I want to give a shout out to Vimjoyer specifically. They do really great videos on YouTube. Highly recommend checking them out. And just a thanks to everyone who gave me feedback on this talk and improved a lot better than it was, I think version one. And thanks to comp 42 for giving me the chance to talk about this. I really like talking about Nix and want to share it with other people to see let them know what they can do. And thanks to you of course, for seeing through this talk and listening to me ramble on about NyX. Hopefully you found that useful and you'll give Nick's a try. Thank you very much. Have a lovely day and enjoy the rest of the conference.

Haseeb Majid

Backend Software Engineer @ Curve

Haseeb Majid's LinkedIn account

Awesome tech events for

Priority access to all content

Video hallway track

Community chat

Exclusive promotions and giveaways