Earlier this month I mentioned on Mastodon that I was replacing a Docker-based local development environment at my day job with a Nix-based one, orchestrated with overmind and a justfile.

There was quite a lot of interest in particular in how overmind and just could be used to replace a container / compose-based local development.

While I can’t share the details of the significantly more complex migration I did at my day job (yet! - I’m working internally on trying to find a way that we can disseminate the learnings publicly), I can share a simplified real-world example that I use for developing Notado.

Let’s take a look at my shell.nix:

{pkgs ? import (fetchTarball "https://nixos.org/channels/nixos-unstable/nixexprs.tar.xz") {}}:
with pkgs; let
  pkgs-2023_03_11 = import (builtins.fetchTarball {
    url = "https://github.com/NixOS/nixpkgs/archive/8ad5e8132c5dcf977e308e7bf5517cc6cc0bf7d8.tar.gz";
  }) {};

  meilisearch-1_0_2 = pkgs-2023_03_11.meilisearch;
  mkShell {
    name = "notado";

    MEILI_MASTER_KEY = "default";
    MEILI_DB_PATH = "data.ms";
    PGDATA = "data.pg";

    buildInputs = [

This pulls in:

  • Packages for the two main data stores, PostgreSQL and Meilisearch
  • Tooling related to the main languages used to write Notado (Rust, Go, Typescript)
  • Orchestration tooling (just, overmind)

While also setting some environment variables which ensure that the data directories for the data stores have recognizable names in the root directory of the monorepo.

overmind is used in the Notado local development environment to orchestrate data stores. Notado has three services related to the data stores: PostgreSQL, Meilisearch and a PostgreSQL -> Meilisearch listener which syncs data from the former to the latter.

Here is the Procfile used by overmind:

meilisearch: meilisearch
postgres: postgres -k /tmp

Meilisearch doesn’t really need in arguments in our case for local development, and PostgreSQL takes a single -k /tmp flag to set the Unix domain socket location. I don’t include the listener here because I don’t always need it running and if I’m working on it, I often have to recompile to see new changes, which doesn’t make it a good fit to live here.

Running overmind start brings up the processes defined in the Procfile for us, similarly to how docker-compose up might work if we were using a container-based local environment.

system      | Tmux socket name: overmind-notado-sqlAl1e6xKxH6K6Ayr3sU
system      | Tmux session ID: notado
system      | Listening at ./.overmind.sock
postgres    | Started with pid 2073401...
meilisearch | Started with pid 2073398...
postgres    | postgres -k /tmp
meilisearch | meilisearch
meilisearch | 
meilisearch | 888b     d888          d8b 888 d8b                                            888
meilisearch | 8888b   d8888          Y8P 888 Y8P                                            888
meilisearch | 88888b.d88888              888                                                888
meilisearch | 888Y88888P888  .d88b.  888 888 888 .d8888b   .d88b.   8888b.  888d888 .d8888b 88888b.
meilisearch | 888 Y888P 888 d8P  Y8b 888 888 888 88K      d8P  Y8b     "88b 888P"  d88P"    888 "88b
meilisearch | 888  Y8P  888 88888888 888 888 888 "Y8888b. 88888888 .d888888 888    888      888  888
meilisearch | 888   "   888 Y8b.     888 888 888      X88 Y8b.     888  888 888    Y88b.    888  888
meilisearch | 888       888  "Y8888  888 888 888  88888P'  "Y8888  "Y888888 888     "Y8888P 888  888
meilisearch | 
meilisearch | Config file path: "none"
meilisearch | Database path:            "data.ms"
meilisearch | Server listening on:      "http://localhost:7700"
meilisearch | Environment:              "development"
meilisearch | Commit SHA:               "unknown"
meilisearch | Commit date:              "unknown"
meilisearch | Package version:  "1.0.2"
meilisearch | 
meilisearch | A master key has been set. Requests to Meilisearch won't be authorized unless you provide an authentication key.
meilisearch | 
meilisearch | 
meilisearch |  Meilisearch started with a master key considered unsafe for use in a production environment.
meilisearch | 
meilisearch |  A master key of at least 16 bytes will be required when switching to a production environment.
meilisearch | 
meilisearch | 
meilisearch | We generated a new secure master key for you (you can safely use this token):
meilisearch | 
meilisearch | >> --master-key YYzxDVu7YNEHyozuGU2itFVW-vnkvAzQVbCMCeOxZzI <<
meilisearch | 
meilisearch | Restart Meilisearch with the argument above to use this new and secure master key.
meilisearch | 
meilisearch | Documentation:            https://docs.meilisearch.com
meilisearch | Source code:              https://github.com/meilisearch/meilisearch
meilisearch | Contact:          https://docs.meilisearch.com/resources/contact.html
meilisearch | 
postgres    | 2023-07-21 19:30:20.552 UTC [2073407] LOG:  starting PostgreSQL 15.2 on x86_64-pc-linux-gnu, compiled by gcc (GCC) 12.2.0, 64-bit
postgres    | 2023-07-21 19:30:20.554 UTC [2073407] LOG:  listening on IPv6 address "::1", port 5432
postgres    | 2023-07-21 19:30:20.554 UTC [2073407] LOG:  listening on IPv4 address "", port 5432
meilisearch | [2023-07-21T19:30:20Z INFO  actix_server::builder] Starting 12 workers
meilisearch | [2023-07-21T19:30:20Z INFO  actix_server::server] Actix runtime found; starting in Actix runtime
postgres    | 2023-07-21 19:30:20.560 UTC [2073407] LOG:  listening on Unix socket "/tmp/.s.PGSQL.5432"
postgres    | 2023-07-21 19:30:20.570 UTC [2073433] LOG:  database system was shut down at 2023-07-21 01:33:23 UTC
postgres    | 2023-07-21 19:30:20.585 UTC [2073407] LOG:  database system is ready to accept connections

These processes will keep chugging along. You can also have them running in detached mode if you prefer.

The last thing that pulls this together is a justfile, which consists of commands that I run either as one-offs, or for services that I stop and start regularly during development:

    initdb -D data.pg -U postgres

    cd rust && diesel migration run

    cd rust && diesel migration revert

    cd rust && diesel migration revert --all

    cd go/listener && go run main.go

    cd rust/notado && cargo run
  • initdb is a one-off that is run to initialize a fresh PostgreSQL database, which I need to do whenever I nuke the data.pg directory
  • migrate, revert, and revert-all are database migration commands
  • listener starts the PostgreSQL -> Meilisearch listener which syncs data
  • notado starts the web server

That’s it, the whole local development environment! Simple, elegant, portable and best of all, no containers!

While I initially used Docker containers to deploy Notado, first to a Kubernetes cluster, and then later to Fly.io, I now deploy the binaries built by Nix directly to a server running NixOS and manage the services with systemd.

If you have any questions you can reach out to me on Mastodon and Twitter.

If you’re interested in what I read to come up with solutions like this one, you can subscribe to my Software Development RSS feed.

If you’d like to watch me writing code while explaining what I’m doing, you can also subscribe to my YouTube channel.

Edit: There has been a lot of great discussion going on over at Tildes about why you might want to do something like this, which is also worth reading!