A sane(r) setup for Python project

Docker has become the way of setting up a development environment for any project. Why?

  • Operating System dependencies - Dockerfile can precisely define what should be starting OS and what needs to be installed,
  • Environment Variables and other files - both filesystem and environment variables can be modified at build and runtime to configure services,
  • Orchestration - Docker Compose (for development) and Kubernetes (for production) allow you to run multiple services at once - e.g. database, web server, reverse proxy and cache,

All in all, pretty compelling option - especially when you have no alternative - or your alternative is using infrastructure configuration tools (like Puppet / Chef / Ansible) or doing it all by hand.

Let’s look at what’s lost though:

  • persistent configuration - if you need something to stay there between multiple runs, it needs to be mounted from the host,
  • easy networking - Docker virtualises the network - especially on Windows / macOS where it adds extra VM to the process - making debugging extra nasty,
  • native execution - although there isn’t a lot of overhead, certain tasks are much harder to do inside Docker than if the binary was running natively - e.g. debugging,

When we started looking for an alternative in OpenDesign, here’s what we came up with:

  • nix for package management,
  • direnv for configuration,
  • s6 for services,

What nix solves?

Nix - the package manager - is an alternative to dictating a single OS via Dockerfile. Instead, it’s a package manager that works on both Linux and macOS - both x64 and arm. It doesn’t require root to invoke and can create isolated environments for each project.

It also adds lock files and the ability to roll back upgrades.

What direnv solves?

Direnv hooks into your shell to allow projects to modify the current shell environment via .envrc files. This means that if you enter a project directory and run direnv allow (always inspect .envrc first!) you get everything configured:

  • it can invoke nix to install dependencies,
  • set environment variables,
  • create configuration files,

What s6 solves?

Once you have everything installed and configured, it’s time to run it - and here s6 is handy. It’s a lightweight and maintained alternative to something like http://supervisord.org or systemd - but it can easily run as a normal user and is configured via a set of directories. Each directory should contain a run file. Each run file corresponds to a single service which will be spawned by s6. Simple and easy to configure.

Tying it all together

Usually, a project is a Nix Flake. numtide-devshell creates a project skeleton already configured with direnv. From that, it’s only a matter of adding s6 to dependencies and creating run files for each service.

Of course, this is just what you need to avoid Docker. To have a full project you also need a couple of extra things.

Everything else

Dependency management - in this case, we are using rather mundane tools. yarn (via corepack - see https://github.com/CodeWitchBella/corepack-overlay) for Node.js and poetry for Python - although from what I’m hearing pdm might be worth keeping an eye on.

For both languages, direnv has a helper: layout python and layout node to adjust PATH and, in the case of Python, create virtualenv.

Secrets - we tend to configure projects via environment variables, so what we essentially need is a helper for direnv to encrypt certain files in the project. sops fits the bill perfectly (and can be installed via Nix, like the rest of the tools). On top of that, we use either GPG (for projects with just a couple of people), age (for machine access) or AWS KMS (for projects with a large number of people).


Tags: ceros backend python nix


Copyright © 2025 L Czaplinski
Powered by Cryogen
Theme by KingMob