William Mulianto

Justfile vs Makefile — Which Task Runner Should You Use?

· 5 min read

I’ve been using just as my task runner for a while now — I wrote about it in a previous post. But I still see plenty of projects using Makefiles for the same purpose: running dev servers, building containers, executing migrations, deploying. Both tools work. Here’s how they compare and when each one makes sense.

What They Have in Common

Both just and make let you define named commands (recipes/targets) in a file at the project root. You run them from the terminal with a single command. They both support dependencies between tasks, variables, and comments.

The overlap is big enough that many developers use Makefiles as task runners without ever touching Make’s actual build system features. That’s fine — but it also means they’re fighting against Make’s design assumptions.

Syntax

Make was designed for building C programs in the 1970s. It shows.

A Makefile:

.PHONY: dev build deploy

dev:
	cd frontend && npm run dev &
	cd backend && npm run dev &
	wait

build:
	docker build -t myapp .

deploy: build
	docker push myapp

The equivalent justfile:

# Start dev servers
dev:
    cd frontend && npm run dev &
    cd backend && npm run dev &
    wait

# Build Docker image
build:
    docker build -t myapp .

# Build and deploy
deploy: build
    docker push myapp

They look similar, but the differences matter in practice:

  • Make requires tabs for indentation. Spaces break silently. This trips up every developer at least once. Just accepts either.
  • Make needs .PHONY for targets that aren’t files. Without it, if you happen to have a file called build in your directory, make build will say “nothing to do.” Just doesn’t have this concept — recipes are always commands, never file targets.
  • Comments in justfiles appear in just --list. You get a built-in help system for free. Make doesn’t have a native equivalent.

Variables and Arguments

Just handles arguments more naturally:

# Deploy to a specific environment
deploy env:
    ./deploy.sh {{env}}
just deploy staging

In Make, passing arguments to targets requires workarounds:

deploy:
	./deploy.sh $(ENV)
make deploy ENV=staging

It works, but the syntax is clunky compared to positional arguments. Just also supports default values, variadic arguments, and environment variable loading from .env files without extra tooling.

Error Handling

By default, Make continues executing a recipe even if a command fails (unless you set specific flags). Just stops on the first error. For task runner use cases, stopping on failure is almost always what you want. You can override this in both tools, but Just’s default is saner for development workflows.

Cross-Platform Support

Make is available everywhere — it comes pre-installed on most Linux distros and macOS. On Windows, you need to install it through MinGW, WSL, or similar, and behavior can differ.

Just is a single binary with no dependencies. Install it with your package manager (brew install just, cargo install just, or download the binary). It works the same on Linux, macOS, and Windows. No platform-specific quirks.

Built-in Features Just Has

A few things that just handles out of the box:

  • just --list — Lists all recipes with their comments. Great for onboarding new team members.
  • just --choose — Interactive recipe picker using fzf if available.
  • .env loading — Add set dotenv-load and just reads your .env file automatically.
  • Conditional expressionsif/else in recipes without shell gymnastics.
  • [private] recipes — Hide helper recipes from the listing.
  • Shebang recipes — Use any language for a recipe body (Python, Node, etc.) by adding a shebang line.

Make can do some of these things, but they require workarounds or shell tricks.

When Make Still Makes Sense

Make is a legitimate build system, not just a task runner. If your project involves:

  • Incremental builds — Make’s dependency tracking and file-based targets are designed for this. It knows not to rebuild something if the source hasn’t changed. Just doesn’t do this at all.
  • C/C++ projects — Make is the standard. The ecosystem expects it.
  • No extra dependencies — Make is already installed. For open-source projects where you want zero setup friction, a Makefile means one less thing to install.
  • Existing team familiarity — If the team knows Make well, switching to just for marginal gains isn’t worth the churn.

When to Use Just

If you’re using a Makefile purely as a command runner — no file targets, no incremental builds, just named shell commands — just is a better fit. The syntax is cleaner, the defaults are better, and you get features like --list and .env loading without hacking them in.

For most modern development workflows — running dev servers, building containers, executing database migrations, deploying — just does everything you need with less friction.

I use a justfile in every project now. It’s the first file I create after git init. The simplicity of running just dev and having everything start up is worth the one-time brew install just.

Related