Improve your Dev Stack with process-compose

As developers, we often run multiple services to work on a single project. I've managed my projects with makefiles, PM2, Tmux, Procfiles, and many other solutions. They all had their pros and cons, but none of them felt like the perfect solution. I always kept looking for a "docker (compose) for development environments". The problem with docker itself is that it works fine for a database, but not for something like go or node services, where you want to restart the service on file changes, and use hot reloading.

In this article, I'll introduce you to process-compose, a tool that identifies itself as follows:

Process Compose is a simple and flexible scheduler and orchestrator to manage non-containerized applications.

And that's exactly what it is. It's a single binary that enables you to define your services in a process-compose.yaml file, and then start them all with a single command. It's like docker-compose, but for binaries on your system. Complete with health checks, restarts, and logs like we know from docker-compose, and with support for hot reloading like you depend on in your local development environment.

Before we set things up, let's take a quick look at an example screen from a bigger project of mine. This is what process-compose looks like:

process-compose session

I like the TUI, but it can be disabled with -t=false if that's not your thing. Plus, logs can be tailed in another terminal with process-compose process logs $process.

Let's get started

You can install process-compose using brew, or by downloading the binary from the releases page. See their installation page for other options.

brew install f1bonacc1/tap/process-compose

Once you have process-compose installed, you can create a process-compose.yaml file in your project root. Here's an example of a process-compose.yaml file for a random node.js project:

version: "0.5"

processes:
  app:
    command: npm run dev

Next, run process-compose up in the same directory as your process-compose.yaml file. This will start the apps process, and you should see the output of the npm run dev command in your terminal. If you don't like the visual output, you can run process-compose up -t=false to run the process without TUI.

That's it in essence, add as many processes as you like. For example one to run your db server, one to start your tests in watch mode, or one to compile your css using postcss.

A more complete example could look like this:

processes:
  app:
    command: npm run dev
    environment:
      - "MONGO_URI=mongodb://localhost:27017"
  postcss:
    command: postcss input.css -o output.css
  mongo:
    command: mongod --dbpath ../.db

Adding the environment variables to this file is completely optional, you can keep using your current preferred way of setting environment variables.

Process Dependencies

You can also define dependencies between processes. For example, you might want to start your database before starting your app. You can do this by adding a depends_on key to your process.

processes:
  app:
    command: npm run dev
    # …
    depends_on:
      mongo:
        condition: process_started
  mongo:
    command: mongod --dbpath ../.db

Run process-compose up mongo when you only want to start the mongo process, and not the app process. When running process-compose up app it will start the mongo process first, and then the app process as you've told it that app cannot run without mongo.

Health checks

It's possible to define health checks for your processes. This helps with two things. By defining a health check, your process can automatically restart when it becomes "unhealthy". It also makes it possible for depending services to delay their start until the process is truly ready. Think of the time between starting your database and it being ready to accept connections.

A health check is defined by adding a readiness_probe key to your process.

processes:
  # …
  mongo:
    command: mongod --dbpath ../.db
    readiness_probe:
      exec:
        command: 'mongosh --eval "db.stats()"'

Add an availability key to make sure the process is restarted when it becomes "unhealthy":

processes:
  # …
  mongo:
    command: mongod --dbpath ../.db
    readiness_probe:
      exec:
        command: 'mongosh --eval "db.stats()"'
    availability:
      restart: on_failure

To make the app process wait for mongo to not just be started, but also be ready, you can add change the depends_on.condition to process_healthy.

processes:
  app:
    command: npm run dev
    # …
    depends_on:
      mongo:
        condition: process_started
        condition: process_healthy

And while we're at it, we can also make sure that the app process is restarted when it crashes. How the health check is done is up to you, but for most servers, a simple curl would do. Combine it with a grep if you'd like.

processes:
  app:
    command: npm run dev
    # …
    readiness_probe:
      exec:
        command: 'curl http://localhost:3000 | grep -i "<!doctype html>"'

That's it. Now run process-compose up, and you'll see the database starting first, and the app as soon as the database is "healthy".

There are a couple options available to configure the max number of errors and retries available for health checks, but I'll forward you to their documentation for that.

Working directory

One more quick tip for if you're working with a mono-repo, or when you're simply not ready to commit the process-compose.yaml file to your project. You can specify the working directory per service, like:

processes:
  app:
    command: npm run dev
    working_dir: ./packages/docs
    # …
  mongo:
    command: mongod --dbpath ./my-app
    working_dir: ~/mongo-data
    # …

This makes sure that the command is run in the context of the working_dir, think cd $working_dir && sh -c $command.

Final words

I've been using process-compose for a couple of weeks now, and I'm loving it. It's a simple tool that does exactly what I need. Power on my machine, run process-compose up, and all my services are started and running in watch mode.

To get a complete Docker-like experience, including package and version management, the "reproducible and isolated environments", check my article about Devbox. You'll love it!

Liked this article?

If you made it to here, please share your thoughts on BlueSky, or leave a comment below.