2.9k
Connect
  • GitHub
  • Mastodon
  • Twitter
  • Slack
  • Linkedin

Blog

Floxifying a Project Repo Part 1

Steve Swoyer | 21 January 2025
Floxifying a Project Repo Part 1

Flox gives you a way to create isolated dev environments that run directly on your local laptop. This means no more creating and maintaining dev containers or VM images, no more configuring container-specific networks or local storage mount points, or sacrificing performance when working on macOS.

Even better, with Flox you get transparent access to all the resources available to you on your local system—like files, secrets, environment variables, even specialized hardware like GPUs.

And once you’ve developed, built, and tested your app or service locally, Flox makes it easy to run the exact same environment in CI—whether on native runners, in containers or VMs, or on platforms like AWS Lambda.

But rather than telling you that this works, let’s walk through how it works.

What does it look like to Floxify a software project? How much work is involved? How can you build locally using Flox and test in CI, or run in prod, using containers, VMs, or lambdas? Read on to find out.

Floxifying your front- and back-end

Recently, your team has been experimenting with a tool called Flox, software that’s supposed to make it easier to create and share development environments. You’re skeptical about this, mostly because whenever you’ve used virtual environment managers or virtualization technologies, each has had major drawbacks.

But Flox is based on open source Nix, which you’ve heard of, and which seems intriguing. So maybe there’s something to it? Logging into Jira, you see you’ve been assigned two new tasks:

  • You’re supposed to “floxify” the Dockerfile for your org’s front-end dev container;
  • You’re supposed to “floxify” the Dockerfile for your org’s back-end dev container.

Knowing next to nothing about Flox, you expect to spend the next month building, testing, and debugging these environments to see if it delivers on its promise. Sipping your coffee, you start by reading through the Flox Docs, familiarizing yourself with what Flox is and how it works.

The big reveal: no containers or VMs required

You discover that Flox delivers reproducibility and isolation without using containers or VMs.

This makes sense. A friend who knows Nix once showed you how running nix-shell puts you right into a project-specific subshell. Flox builds on this concept, abstracting the nuts-and-bolts of Nix so you can easily run software in isolated subshell environments. With Flox, you can declaratively define everything in your environment: software packages, environment variables, setup and teardown logic, services, even aliases and functions. Flox environments run on your local system, not in hermetically sealed containers or VMs, so you can also transparently access all your files, data, secrets, aliases, environment variables—everything.

Interesting. That alone should make developing and testing locally much more friction free.

But there’s gotta be a catch, right? You keep reading through the docs, looking for the gotcha.

Scoping the project

After a half-hour or so of reading, you go to the office Keurig, pop in a Coconut Mocha Peppermint Spice K-cup, and poke the “BREW” button.

While your cuppa’s compiling, you realize that you can probably map the runtime dependencies specified in your front-end app’s docker-compose.yaml file to equivalent packages in the Flox Catalog.

Your app gets much of what it needs from npm at runtime, and you know Flox has built-in automation that bootstraps and installs the version of Node.js specified in package.json. And you can get other core dependencies—like playwright, ngnix, jq, even the AWS and GitHub CLIs, from the Flox Catalog itself.

You’d define these under the [install] section of manifest.toml, Flox’s default configuration artifact:

[install]
nodejs.pkg-path = "nodejs"
nodejs.version = "20.18.1"
gitFull.pkg-path = "gitFull"
gh.pkg-path = "gh" # github cli
awscli2.pkg-path = "awscli2"
bash.pkg-path = "bash" # standard version of bash
curl.pkg-path = "curl"
httpie.pkg-path = "httpie"
jq.pkg-path = "jq"
playwright.pkg-path = "playwright" # for browser automation and end-to-end testing
_1password.pkg-path = "_1password" # 1password cli for secrets management

You’d do something similar for any global, environment-specific variables defined in docker-compose.yaml, like NODE_ENV=development, VITE_API_URL, DATABASE_URL, and LOG_LEVEL=debug. You note that the Flox manifest has a special section just for these, called [vars].

It has a separate section just for services, too—called, aptly enough, [services]. Here you define the start-up logic used to spin up (or, optionally, stop) required services as part of your dev container runtimes. If your services require environment variables of their own, you can define these in [vars].

This lets you offload the work of starting, stopping or restarting runtime services to Flox’s service manager.

Plus, with Flox, you don’t have to worry about orchestrating a multi-container runtime or configuring service-specific networking or API endpoints for containers. And you no longer need to define entry points for initializing containers, since services run natively on the host, “inside” each isolated Flox environment.

Similarly, you can implement any scripting logic expressed in docker-compose.yaml in the [hook] or [profile] sections of manifest.toml. This includes setup/teardown wizards and other automations.

Build locally, share globally

Because Flox environments run as isolated subshells under the local user, they can transparently access anything the local user has permission to use without elevating privileges. This means they inherit available variables (unless an environment’s manifest specifies overrides), can access local secrets, and so on.

You take a deep breath: No more figuring out hacks—um, “patterns”—for injecting variables and secrets into dev containers!

All of this feels easy, straightforward—organic—in precisely the way that using containers or VMs does not.

Then, when you take the first sip of your fresh Coconut Mocha Peppermint Spice Latte, you have an epiphany: With Flox, you can define both the code for your apps and their runtimes in the same GitHub repo.

This makes it … surprisingly straightforward to build, test, and deploy applications or services along with their runtime dependencies as part of a single, integrated workflow.

Wow! To git clone the code for a project’s repo is to git clone the runtime for that repo. That’s useful!

Time-invariant reproducibility

As for the Flox environment, you learn that it consists of just two artifacts:

  • A manifest.toml file that lives in the .flox/env path inside every Flox project folder;
  • A manifest.lock file that lives in .flox/env and serves as a snapshot of specific versions of runtime dependencies.

You know about manifest.toml, because that’s where you define the runtime dependencies and services specified in your docker-compose.yaml. But manifest.lock is a new concept. Flox uses this to pin dependencies to specific versions. This means Flox environments (like Nix shells) are always reproducible, because each is identified by a cryptographic hash derived from its build inputs—source code, packages, build instructions, and environment variables. This immutable information lives in the Nix store, which is located at /nix/store.

Any change to a Flox environment’s build inputs results in a different, immutable output path in /nix/store, producing a different cryptographic hash. So if you create a Flox environment with pinned versions today, that environment will use the same software—and run exactly the same way—two, three, or five years later.

You know from experience that this isn’t easily possible with Dockerfiles, which depend on base images that change frequently and dynamically resolve and/or build package versions. Like Nix, Flox gives you a way to decouple runtime definitions from their system context, something that just isn’t possible with containers.

Lift-and-shift package requirements

Still savoring your Coconut Mocha Peppermint Spice Latte, you turn to the task of re-implementing the dev container you use for your backend services as a Flox environment. Here again, you’ll get most of what you need from npm, with Flox automatically detecting and installing dependencies defined in package.json.

As for other requirements, you’ll define these in the [install] section of manifest.toml:

[install]
nodejs.pkg-path = "nodejs"
nodejs.version = "20.18.1"
redis.pkg-path = "redis"
postgresql_16.pkg-path = "postgresql_16" # postgresql instance for backend runtime
gitFull.pkg-path = "gitFull"
gh.pkg-path = "gh" # github cli
bash.pkg-path = "bash" # standard version of bash
curl.pkg-path = "curl"
jq.pkg-path = "jq"
gcc.pkg-path = "gcc" # compiler for gnumake
gnumake.pkg-path = "gnumake"
_1password.pkg-path = "_1password" # 1password cli for secrets management

You can configure both postgresql and redis as Flox services.

Here’s what this looks like in the [services] section of manifest.toml:

postgres.command = "postgres -D $PGDATA -c unix_socket_directories=$PGHOST -c listen_addresses=$PGHOSTADDR -p $PGPORT"
redis.command = "redis-server $REDISCONFIG --daemonize no --dir $REDISDATA"
nodejs.command = "npm run dev"

You’ll define required environment variables and put them in the [vars] section of manifest.toml:

REDISBIND = "127.0.0.1"
REDISPORT = "16379"
PGHOSTADDR = "127.0.0.1"
PGPORT = "15432"
PGUSER = "pguser"
PGPASS = "pgpass"
PGDATABASE = "pgdb"

You’ll also author bash bootstrapping logic to automatically initialize, configure, and set up PostgreSQL and Redis, checking for and creating required data directories and generating config files if missing.

For Redis, your logic might look like this:

[hook]
on-activate = '''
unset LD_AUDIT
 
export REDISHOME="$FLOX_ENV_CACHE/redis"
export REDISDATA="$REDISHOME/data"
export REDISCONFIG="$REDISHOME/redis.conf"
 
if [ ! -d "$REDISDATA" ]; then
  mkdir -p "$REDISDATA"
fi
 
cat >$REDISCONFIG <<EOF
bind $REDISBIND
port $REDISPORT
EOF
 
'''

And with that … you’d be done.

Or are you? You’ve re-implemented your local dev containers as Flox environments that run transparently on your laptop. But how are you supposed to replicate what you’ve done in CI—or in production?

Complementing containers

That’s what the flox containerize command is for. It automatically exports Flox environments as container images, compressing them into tarballs and/or optionally exporting them to an available container runtime.

Your platform team has defined variables for distinguishing an app or service’s runtime context (e.g., NODE_ENV and APP_ENV), and for context-specific APIs (like local versus ci versus prod API endpoints), so in theory could just redefine these in your Flox manifests and run flox containerize:

NODE_ENV=ci
APP_ENV=ci
API_BASE_URL=${BASE_URL}

Come to think of it, however, you don’t need to do this at all! You can just...

git commit -s -m “floxified_version_of_frontend”
git commit -s -m “floxified_version_of_backend”

...and create PRs for both repos. This is much easier than the pattern you now use to deploy containers to CI!

In fact, your platform team has created a GitHub Action for building and deploying Flox container images in CI. This action automates the process of cloning a GitHub repo; defining environment variables for the target context (e.g., ci); running flox containerize to build the container; and executing it using a pre-defined runner.

Flox makes it just as easy to author Lambda functions, test them locally, and push them to CI!

This workflow is possible because you can define both code and runtime dependencies in one place.

“Why don’t more people know about this?” you think to yourself, swallowing the last sip of Coconut Mocha Peppermint Spice goodness. "Okay," you concede,"Flox seems like its own kind of goodness, too."

Flox helps you build software $PATH independently

Flox transforms how teams develop, build, and test software locally, eliminating the need for dev containers or VMs. Flox environments run in isolated subshells in the local system context, so developers enjoy transparent access to local files, secrets, environment variables, and other resources.

With Flox, teams can declaratively define dependencies, variables, services, and other logic in one place: a Flox environment’s manifest. The upshot is that when you flox init to create a new Flox environment “inside” a GitHub repo, you’re defining both your code and its runtime dependencies in the same place.

Best of all, your Flox environments always run and behave the same way—everywhere: locally, in CI, and in production. Because unlike Dockerfiles or container orchestration manifests, Flox gives you a declarative way to decouple an app or service’s runtime dependencies from the system context in which it executes.

Flox is free and easy to learn and use. If what you’ve read piques your interest, consider downloading it and Floxifying one of your own project repos!