Ditching Docker for Local Development
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 thedata.pg
directorymigrate
,revert
, andrevert-all
are database migration commandslistener
starts the PostgreSQL -> Meilisearch listener which syncs datanotado
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!