Setting up a dev environment for PostgreSQL with nixos-container

I’ve been using NixOS for about a month now, and one of my favourite aspects is using lorri and direnv to avoid cluttering up my user or system environments with packages I only need for one specific project. However, they don’t work quite as well when you need access to a service like PostgreSQL, since all they can do is install packages to an isolated environment, not run a whole RDBMS in the background.

For that, I have found using nixos-container works very well. It’s documented in the NixOS manual. We’ll be using it in ‘imperative mode’, since editing the system configuration is the exact thing we don’t want to do. You will need sudo/root access to start containers, and I’ll assume you have lorri and direnv set up (e.g. via services.lorri.enable = true in your home-manager config).

We’ll make a directory to work in, and get started in the standard lorri way:

$ mkdir foo && cd foo
$ lorri init
Jul 11 21:23:48.117 INFO wrote file, path: ./shell.nix
Jul 11 21:23:48.117 INFO wrote file, path: ./.envrc
Jul 11 21:23:48.117 INFO done
direnv: error /home/josh/c/foo/.envrc is blocked. Run `direnv allow` to approve its content
$ direnv allow .
Jul 11 21:24:10.826 INFO lorri has not completed an evaluation for this project yet, expr: /home/josh/c/foo/shell.nix
direnv: export +IN_NIX_SHELL

Now we can edit our shell.nix to install PostgreSQL to access it as a client:

# shell.nix
{ pkgs ? import <nixpkgs> {} }:

pkgs.mkShell {
  buildInputs = [
    pkgs.postgresql
  ];
}

Save that and lorri will start installing it in the background.

Now, we can define our container, by providing its configuration in a file. I have called it container.nix, but I don’t think there’s a standard name for it like there is for shell.nix. Here it is:

# container.nix
{ pkgs, ... }:

{
  system.stateVersion = "20.09";

  networking.firewall.allowedTCPPorts = [ 5432 ];

  services.postgresql = {
    enable = true;
    enableTCPIP = true;
    extraPlugins = with pkgs.postgresql.pkgs; [ postgis ];
    authentication = "host all all 10.233.0.0/16 trust";

    ensureDatabases = [ "foo" ];
    ensureUsers = [{
      name = "foo";
      ensurePermissions."DATABASE foo" = "ALL PRIVILEGES";
    }];
  };
}

It’s important to make sure the firewall opens the port so that we can actually access PostgreSQL, and I’ve also installed the postgis extension for geospatial tools. The authentication line means that any user on any container can authenticate as any user with no checking: fine for development purposes, but obviously be careful not to expose this to the internet! Finally, we set up a user and a database to do our work in.

Now, we can actually create and start the container using the nixos-container tool itself. This is the only step that requires admin rights.

$ sudo nixos-container create foo --config-file container.nix
$ sudo nixos-container start foo

By now, lorri should have finished installing PostgreSQL into your local environment, so once nixos-container has finished running, you should be able to access the new database inside the container:

$ psql -U foo -h $(nixos-container show-ip foo) foo
psql (11.8)
Type "help" for help.

foo=> \l
                                  List of databases
   Name    |  Owner   | Encoding |   Collate   |    Ctype    |   Access privileges
-----------+----------+----------+-------------+-------------+-----------------------
 foo       | postgres | UTF8     | en_US.UTF-8 | en_US.UTF-8 | =Tc/postgres         +
           |          |          |             |             | postgres=CTc/postgres+
           |          |          |             |             | foo=CTc/postgres
 postgres  | postgres | UTF8     | en_US.UTF-8 | en_US.UTF-8 |
 template0 | postgres | UTF8     | en_US.UTF-8 | en_US.UTF-8 | =c/postgres          +
           |          |          |             |             | postgres=CTc/postgres
 template1 | postgres | UTF8     | en_US.UTF-8 | en_US.UTF-8 | =c/postgres          +
           |          |          |             |             | postgres=CTc/postgres
(4 rows)

And there we go! We have a container that we can access from the command line, or from an app, and we didn’t need to install PostgreSQL globally. We can even have multiple containers like this for different projects, and they’ll all use the same Nix store for binaries but have completely isolated working environments.

The nixos-container tool itself is a fairly thin wrapper around systemd (the containers themselves work via systemd-nspawn). The containers won’t auto-start, and you have to use systemctl to make that happen:

$ sudo systemctl enable container@foo.service

As a final flourish, we can save having to type in the user, host IP and database with very little effort, since we’re already using direnv and most tools can take their PostgreSQL configuration from some standard environment variables. We just have to add them to our .envrc file, and then re-allow it.

$ cat .envrc
PGHOST=$(nixos-container show-ip foo)
PGUSER=foo
PGDATABASE=foo
export PGHOST PGUSER PGDATABASE

eval "$(lorri direnv)"
$ direnv allow .
$ psql
psql (11.8)
Type "help" for help.

foo=>