Local Services with asdf and foreman

  • January 19, 2022
  • Erik Cameron
  • 3 min read

Upon learning of asdf plugins for backing services like Postgres, Redis, Elasticsearch and others, my first question was whether and to what extent it could replace Docker for providing those services in local development, where asdf already shines for versioning.

The first obstacle for most users will be that asdf has no equivalent of docker compose up for booting and monitoring an entire local environment. (As of January 2022 the asdf discussions page is recently dark but this was an active topic at the time of its disappearance.) Brainstorming the matter with a colleague, he pointed out we were essentially proposing Foreman. A few moments later he found this:

Managing Ruby on Rails development dependencies with asdf and foreman

[Note: We're talking about Foreman, not The Foreman.]

From the oldie-but-a-goodie department: Foreman is a simple tool that reads a list of command line tasks from a file, starts them, feeds their output to stdout with some nice formatting, and eventually shuts them down when you exit the process; i.e., exactly what asdf doesn't provide. As outlined above, there isn't even really any "integration" necessary for asdf and Foreman to play nicely. They just do their own things correctly.

I've been using the approach above where possible for a couple of weeks now, and it's nice. (Be sure to note the trick in the blog post for running Postgres under Foreman control!) Docker is, among other things, a significant resource commitment for local development. On a Mac, you're spinning up an entire separate Linux instance to run software that MacOS is quite capable of running itself. This might be a good tradeoff; for many the benefit of standardizing developer experience or duplicating an idiosyncratic production environment will outweigh other considerations. But for individual developers looking for lightweight solutions—or to avoid duplicating functionality already provided by the kernel, the shell, asdf and various language-specific dependency managers—this solution is very much worth a look.

Streamlining

I created the following in ~/bin/app, which will probably get (a little) more clever over time:

#!/usr/bin/env zsh

foreman start -f Procfile.local

I used Procfile.local to prevent conflicts with applications that already have a Procfile for production. In this case we're booting a Phoenix project using Postgres and Redis:

postgres: mkdir -p log/ && postgres-local
redis: redis-server
api: mix phx.server

Meanwhile in .tool-versions:

elixir main-otp-24
erlang 24.2
postgres 14.1
redis 6.2.6

Booting the application is nice and simple:

$ app
17:05:22 postgres.1 | started with pid 12345
17:05:22 redis.1    | started with pid 12346
17:05:22 api.1      | started with pid 12347
[...logging continues, ctrl-c to exit...]

Future possibilities, difficulties, etc.

The main pain point I see is running multiple applications simultaneously. Docker virtualizes an entire operating system, so if you have two applications, A and B, which both use the same version of (say) Postgres, they still get entirely separate installations, data directories, port 5432 to bind, and so on. With this approach, you have one version of Postgres.

One answer is to separate out the runtime specifics of the service from the codebase, which will presumably depend on the nature of the service itself, but follow a general shape:

  • Environment variables
  • Configuration files
  • Persistent data storage (your database)
  • Temporary data storage (logging, lock files)

If you can specify those uniquely for each instance in Procfile.local, it's likely feasible to run multiple instances of the same service simultaneously. My own research into that will occur when/if I need to run several unrelated applications simultaneously, which is not usually how I work.

Credit where credit is due

Big h/t to Kassio Borges for having thought of this before any of us over here. That link again: Managing Ruby on Rails development dependencies with asdf and foreman