- cross-posted to:
- programming@programming.dev
- cross-posted to:
- programming@programming.dev
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; in mkShell { name = "notado"; MEILI_MASTER_KEY = "default"; MEILI_DB_PATH = "data.ms"; PGDATA = "data.pg"; buildInputs = [ alejandra bacon cargo-cache cargo-expand cargo-insta cargo-udeps diesel-cli go just meilisearch-1_0_2 nodePackages.typescript nodePackages.web-ext nodejs openssl overmind pkg-config postgresql_15 rustup terraform tmux ]; }
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 "127.0.0.1", 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: initdb -D data.pg -U postgres migrate: cd rust && diesel migration run revert: cd rust && diesel migration revert revert-all: cd rust && diesel migration revert --all listener: cd go/listener && go run main.go notado: 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!