0xdsqr/posts/misc/about
Dave Dennis (@0xdsqr)
••
Getting Started with Go and Nix Flakes

Getting Started with Go and Nix Flakes

Author
·
November 3, 2025·12 min·NixWithMe
nixgolangflakesdevops
Loading post...

comments (0)

sign in to leave a comment

no comments yet. be the first to share your thoughts.

  • Setting up your project
  • Set up your flake.nix
  • Commit the flake.nix file
  • Ensure you have flakes enabled
  • Start your development shell
  • What is a development shell?
  • Start your `devShell`
  • Initialize your Go project
  • Create your entry point
  • Building your first packages
  • Packaging with derivation
  • Updating your flake.nix
  • Building your package
  • Packaging with containers
  • Updating your flake.nix
  • Building your container
  • Setting up executable commands

As a common theme of this page, you may be able to tell I'm learning Nix. One of the main reasons I've gotten into it is the power of the flake.nix. One of the things I love the most is the build and development reproducibility. Here I'm going to cover some of my favorite things (devShells, packages, containers, etc.) while building a starter project in Golang.

Setting up your project

Let's assume we have a pretty blank project to start. Something you may get if you just created a new git project.

1
2
3
4
➜  nix-starters git:(main) tree
.
├── LICENSE
├── README.md

With a new project like this, before we can get started with anything, we should set up our flake.nix which will enable us to utilize the full power of Nix.

Set up your flake.nix

If you have an empty project, you will need to create a flake.nix file and fill in some basic content. We'll start off with a file that looks like this and then expand on it. There are comments there for you. Also you should not be running anything yet

  • just load in our file and move onto the next steps to commit it.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
{
  description = "🟪 Go 1.24 Project with Nix";
  
  # Define the sources we'll use in our flake
  inputs = {
    # Unstable channel for latest package versions
    # This provides access to the newest available Go and tools
    nixpkgs-unstable.url = "github:NixOS/nixpkgs/nixos-unstable";
    
    # Stable channel for production-ready packages
    # Useful for dependencies that require more stability
    nixpkgs-stable.url = "github:NixOS/nixpkgs/release-24.11";
    
    # Provides utility functions for working with flakes
    # Simplifies handling multiple systems (x86_64-linux, aarch64-darwin, etc.)
    flake-utils.url = "github:numtide/flake-utils";
  };
  
  outputs = { self, nixpkgs-unstable, nixpkgs-stable, flake-utils }:
    flake-utils.lib.eachDefaultSystem (system:
      let
        # Import both package sets for the current system
        pkgs-unstable = import nixpkgs-unstable { inherit system; };
        pkgs-stable = import nixpkgs-stable { inherit system; };
      in {
        # Default development shell with necessary tools
        devShells.default = pkgs-unstable.mkShell {
          buildInputs = with pkgs-unstable; [
            # Core Go development tools
            go_1_24        # Go compiler and runtime (version 1.24)
            gopls          # Official Go language server protocol implementation
            gotools        # Essential Go development utilities
            golangci-lint  # Meta-linter combining 50+ linters in one tool
            delve          # Powerful debugger for Go applications
            git            # Distributed version control system
          ];
          
          # Executed when entering the development shell
          shellHook = ''
            # Set up project-local configuration
            export GOPATH="$PWD/.go"              # Project-local GOPATH
            export PATH="$GOPATH/bin:$PATH"       # Make go install binaries available
            export GO111MODULE=on                 # Ensure modules mode is enabled
            
            # Create directories if needed
            mkdir -p .go/bin
            
            echo "🟪 Go 1.24 development environment activated!"
          '';
        };
        
        # Define a formatter for the flake itself
        # This helps maintain consistent formatting with 'nix fmt'
        # Run with: nix fmt
        formatter = pkgs-unstable.nixpkgs-fmt;
      }
    );
}

There is a component in there devShells.default that we touch on below. Don't worry about that quite yet. Just know you'll need it and we do a deep dive in a moment.

Commit the flake.nix file

This is a required step, please don't miss it. Without this, you will face errors when we run nix develop

Nix flakes require the files to be tracked by git before they can be used:

1
2
git add flake.nix
git commit -m "feat: init nix flake"

While not the focus of this blog, it's my understanding that they require the flake.nix to be tracked by Git for a few reasons:

  • Reproducibility: The primary goal of flakes is to provide reproducible environments. By requiring git tracking, Nix ensures that the exact state of the flake files is recorded and can be reproduced later.

  • Content addressing: Flakes use Git's content-addressing system to identify and reference dependencies. This means they can use the git commit hashes to create stable references, which is how you can use git to host and share your flakes.

  • Lock files: When you use a flake, Nix creates a flake.lock file that records the exact version of all dependencies. This lock file works in tandem with git to ensure the exact same dependencies are used each time.

  • Prevent accidental changes: By requiring committed files, Nix prevents you from accidentally using uncommitted changes in your environment.

  • Remote fetching: The git requirement also enables Nix to easily fetch flakes from remote repositories since they're already in a Git-compatible format.

Ensure you have flakes enabled

For the purposes of this blog, we are assuming you know what flakes are and have them enabled. If you haven't enabled flakes in your Nix configuration and are still trying to follow along, you can temporarily enable them using:

1
nix --experimental-features 'nix-command flakes' develop

Or add this to your ~/.config/nix/nix.conf or /etc/nix/nix.conf to enable permanently:

1
experimental-features = nix-command flakes

Start your development shell

Now as we talked about in your flake.nix, there is a devShells.default that was provided for you. This is going to be the money maker for you in terms of creating a standard reproducible environment.

What is a development shell?

I guess before we start up our first shell, we should cover what it is in case you don't have an understanding. Development shells (devShells) in Nix flakes are isolated, project-specific environments that contain exactly the tools, libraries, and configurations you need for a particular project. You can think of them as purpose-built workspaces that instantly give you access to everything required for development without polluting your global system. Yes, that's right - super powerful. So unlike traditional virtual environments or containers, development shells:

  • Provide exact, reproducible environments down to the binary level
  • Can be shared across team members with perfect consistency
  • Don't require heavyweight virtualization
  • Can be entered and exited without restarting terminals
  • Work identically across Linux, macOS, and other supported platforms

That's all I'll touch on here, but they are super powerful and fun to play around with.

Start your devShell

Now that you have an understanding of what it is, you can start up your dev shell. As you saw in the comments of the flake, when you start up the dev shell, you'll get an environment with everything we have defined such as go 1.24.

1
2
3
➜  nix-flake-golang git:(main) ✗ nix develop
🟪 Go 1.24 development environment activated!
davecave:nix-flake-golang daveved$

Inside the newly activated shell, you can verify we have the packages installed:

1
2
davecave:nix-flake-golang daveved$ go version
go version go1.24.1 darwin/arm64

Initialize your Go project

Now that we have a development shell setup with Nix enabled, we can bootstrap our Go project. I'll just use go mod init. I'll assume you know how to do this. After this you should end up with a file tree like this:

1
2
3
4
5
6
7
8
9
10
11
12
davecave:nix-flake-golang daveved$ go mod init github.com/0xdsqr/nix-starters/nix-flake-golang
go: creating new go.mod: module github.com/0xdsqr/nix-starters/nix-flake-golang
davecave:nix-flake-golang daveved$ ls
flake.lock  flake.nix  go.mod
davecave:nix-flake-golang daveved$ tree
.
├── flake.lock
├── flake.nix
└── go.mod

1 directory, 3 files
davecave:nix-flake-golang daveved$

You would also see a README.md and LICENSE etc. whatever was in your initial project.

Create your entry point

Now that we have our Go project bootstrapped, we can create a simple entry point for our Golang application. We'll just create a main function that prints hello world for now.

1
2
3
4
5
6
7
8
9
10
11
davecave:nix-flake-golang daveved$ mkdir -p cmd/dsqr
davecave:nix-flake-golang daveved$ cat > cmd/dsqr/main.go << 'EOF'
package main

import "fmt"

func main() {
        fmt.Println("Hello, World!")
}
EOF
davecave:nix-flake-golang daveved$

You can use nvim or whatever you want here to create the files, code, etc. For smaller bits like this I just provide helpers if you wish.

Building your first packages

Nix offers a streamlined way to build packages. It's one of the major selling points, and once you get used to it there is no real turning back. Our goal here is not to go into all the details but instead give you some working examples that you can follow along with to learn. We'll cover some of the basics here as we're getting started with a Golang application.

Packaging with derivation

In Nix, a derivation is the fundamental building block for creating packages. Think of it as a recipe that describes:

  1. The inputs needed to build the package
  2. The build steps required
  3. Where the output should go

Nix offers many specialized functions for different languages. For Go, we have several options:

  • stdenv.mkDerivation: The general-purpose derivation builder
  • buildGoModule: Specialized for Go modules with dependency management
  • buildGo118Module: Version-specific Go module builder
  • buildGoPackage: Older style builder for Go packages

For our simple example, we'll use stdenv.mkDerivation as it gives us fine-grained control over the build process. This approach is especially useful when you're learning how derivations work.

When you build a package with Nix, the result (output) is stored in the Nix store under /nix/store/. Each package gets a unique hash based on all its inputs, ensuring reproducibility. This is a core principle of Nix - the same inputs will always produce the same outputs.

Updating your flake.nix

Let's update our flake.nix by adding two key elements:

  1. First, define the app name and build configuration:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# Add inside the 'let' section of your flake
# Define the application name - a string value used across the flake
appName = "dsqr";

# Define application as a variable using mkDerivation
goApp = pkgs-unstable.stdenv.mkDerivation {
  name = appName;          # The name of our package
  src = ./.;               # Use current directory as source - all files
  
  # Build dependencies (not runtime) - these won't be in the final output
  nativeBuildInputs = with pkgs-unstable; [
    go_1_24                # Specific Go version to use for building
  ];
  
  # The actual build commands executed during 'nix build'
  buildPhase = ''
    # Set up temporary Go cache and module paths
    export GOCACHE=$TMPDIR/go-cache
    export GOPATH=$TMPDIR/go
    
    # Change to the application's source directory
    cd cmd/${appName}
    
    # Build the Go application
    go build -o ${appName}
  '';
  
  # How to install the built artifacts into the Nix store
  installPhase = ''
    # Create the bin directory in the output path
    mkdir -p $out/bin
    
    # Copy the built binary to the output bin directory
    cp ${appName} $out/bin/
    
    # Ensure the executable bit is set
    chmod +x $out/bin/${appName}
  '';
};
  1. Second, add the package reference in the outputs section:
1
2
# Add inside the output section, where formatter is defined
packages.default = goApp;

Building your package

Now you can build your package with:

1
nix build

This will create a result symlink in your current directory that points to the built package in the Nix store. You can run your application with:

1
./result/bin/dsqr

If you want to just run it without building it first, you can use:

1
nix run

Packaging with containers

One powerful feature of Nix is the ability to build container images without Docker. This approach provides several benefits:

  • Reproducible builds: Same deterministic builds you expect from Nix
  • Smaller images: More efficient layering and inclusion of only what's needed
  • No Docker daemon needed: Build containers without Docker installed
  • Multi-architecture support: Build for different architectures from the same machine

Nix provides several functions for building images, including:

  • dockerTools.buildImage: Simple single-layer image
  • dockerTools.buildLayeredImage: More efficient multi-layer image
  • dockerTools.streamLayeredImage: Stream output directly to a registry

Updating your flake.nix

Let's add a container package to our flake:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# Add inside the outputs section, after packages.default
packages.container = pkgs-unstable.dockerTools.buildLayeredImage {
  # Basic container metadata
  name = appName;            # Container name (required)
  tag = "latest";            # Container tag (optional, defaults to "latest")
  
  # Contents to include in the image (as layers)
  contents = [
    # Include our built Go application
    self.packages.${system}.default
    # Add busybox for basic shell utilities
    pkgs-unstable.busybox
  ];
  
  # Container configuration (equivalent to Dockerfile settings)
  config = {
    # Command to run when container starts (like ENTRYPOINT + CMD)
    Cmd = [ "${appName}" ];
    # Declare ports that should be exposed (like EXPOSE in Dockerfile)
    ExposedPorts = {
      "8080/tcp" = { };
    };
  };
};

Building your container

You can build the container image with:

1
nix build .#container

This generates a container image in Docker's save format in the result file, which you can load into Docker:

1
docker load < result

A sample output looks like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[dsqr@server:~/nix-starters/nix-flake-golang]$ nix build .#container

[dsqr@server:~/nix-starters/nix-flake-golang]$ docker load < result
bac1ce201147: Loading layer  2.079MB/2.079MB
73c3619fb187: Loading layer  399.4kB/399.4kB
d49827920aca: Loading layer    215kB/215kB
c8eacd12fc04: Loading layer  30.78MB/30.78MB
9d4dc07856d6: Loading layer   1.69MB/1.69MB
38e0af94c91a: Loading layer  583.7kB/583.7kB
3b0071b79b5f: Loading layer  133.1kB/133.1kB
67fe4cdb969c: Loading layer  2.929MB/2.929MB
80ed722b7b56: Loading layer  266.3MB/266.3MB
e14cdee2dc9a: Loading layer  1.423MB/1.423MB
70924302b70c: Loading layer  1.567MB/1.567MB
77846598d2f3: Loading layer    215kB/215kB
Loaded image: dsqr:latest

[dsqr@server:~/nix-starters/nix-flake-golang]$ docker images
REPOSITORY                   TAG       IMAGE ID       CREATED        SIZE
dsqr                         latest    5f36d088d013   55 years ago   291MB

Setting up executable commands

If you wish to use nix run to execute commands without running some form of nix build first, you can use the apps feature. This allows you to define executable commands that can be run with nix run .#myapp or simply nix run for the default app:

1
2
3
4
5
# Add inside the output section, after packages.default
apps.default = {
  type = "app";              # Specifies this is an app definition
  program = "${goApp}/bin/${appName}";  # Full path to the executable
};

You can then run it with nix run and it will handle everything for you:

1
2
davecave:nix-flake-golang daveved$ nix run
Hello, dsqr!

This command builds the package if needed and then executes it directly.