Justfile is twelve lines and saves me hours a week
There is a shell command I run on a client’s Synology NAS whenever they need an update pushed. It is long, it has eight flags, and most of those flags exist because the alternative is wiping a production database. I used to type it from memory. I do not do that anymore.
I run a handful of small services for clients out of self-hosted boxes. Usually a Synology in a closet, a tiny VPS, or both. The deploy story is some flavour of docker compose pull && docker compose up -d, except never that simple, because the moment you have stateful containers in the mix, the wrong flag becomes a phone call from a panicked office manager. I have not made that phone call yet. The reason I have not is a single-file tool called justfile, and I learned about it in the same session Rafiq taught me Docker properly.
Your shell history is a footgun
The mental model most developers carry is that the terminal is a record of the past. It is not. It is a list of partially-correct guesses, ordered by the last time you got away with one. You hit arrow-up, you scan for the command that looks right, you press enter. If three of those commands deploy and one of them rebuilds your database container from scratch, the day you misclick is the day you find out which is which.
The commands that scare me are not the obscure ones. They are the ones that look almost identical to the safe one.
# what I used to type from memory, every deploy
docker compose -f /volume1/docker/internal/compose.yaml \
--env-file /volume1/docker/internal/.env.prod \
up -d --force-recreate --no-deps app api worker
That command is fine. It rebuilds the app, the API, and the worker, and leaves everything else alone. The dangerous version of that command is one word different.
# the one I never want to run by accident
docker compose -f /volume1/docker/internal/compose.yaml \
--env-file /volume1/docker/internal/.env.prod \
up -d --force-recreate
No --no-deps, no service list. That one rebuilds every container, including the MSSQL instance that holds three years of the client’s invoicing data. Same prefix, different ending. Indistinguishable in a shell history scrolled at 11pm.
A shell command you have to remember correctly is a shell command you will eventually run incorrectly.
Justfile is a Makefile that does not hate you
Justfile is a command runner. The syntax is roughly Make without the historical baggage and the tab-character trauma. You write recipes in a single file at the project root and run them with just <recipe>. Recipes are named, documented, and discoverable. just on its own lists every recipe in the project. Tab completion works out of the box.
# justfile
default:
@just --list
deploy:
docker compose -f /volume1/docker/internal/compose.yaml \
--env-file /volume1/docker/internal/.env.prod \
up -d --force-recreate --no-deps app api worker
logs service:
docker compose -f /volume1/docker/internal/compose.yaml logs -f {{service}}
restart service:
docker compose -f /volume1/docker/internal/compose.yaml restart {{service}}
just deploy runs the long command. just logs api tails the API logs. just restart worker restarts the worker. The shell command never has to live in my head again. It lives in a file, in git, version-controlled and visible to anyone else who touches the project. Including future me, six months from now, when I have forgotten everything I am writing in this article.
How I actually use it
A justfile earns its keep when a project has more than two commands you run with any frequency. Most of mine end up with the same shape.
The dev recipe is the first thing I write. just dev boots the local stack, runs migrations if the schema drifted, and tails the logs. New contributor, new laptop, fresh clone of the repo, one command and they are up. No README full of “make sure to run X before Y.”
The logs recipe takes a service name and tails it. I never docker compose logs -f api directly anymore. just logs api is shorter, and it pins the compose file path so the command works the same from any subdirectory in the repo.
The db-shell recipe drops me into a psql or mssql shell with the right credentials pulled from the env file. Before justfile, I had a stickied note in Obsidian with the exact connection string. Now the note is the recipe.
The status recipe is one I added after the third time I tried to deploy to a Synology that was 92% full. just status runs docker compose ps, then prints disk usage, then prints the last backup timestamp. Three boring commands in sequence, but having them under one name means I run them every time, instead of pretending the disk is fine because checking would be a hassle.
The recipe that saved my Synology migration
When I migrated the office’s stack onto the NAS, every service was new and disposable except one. The MSSQL container holds the data the client built their business on. Every other container can be rebuilt without anyone noticing. MSSQL cannot.
So I wrote the deploy recipe to enumerate the safe services explicitly. There is no recipe in the file that touches MSSQL without me typing the words to do it.
# services that are safe to force-recreate
safe_services := "app api worker scheduler proxy"
db_name := "internal_prod"
deploy:
@echo "Deploying: {{safe_services}}"
@echo "MSSQL is excluded by design. Use 'just db-rebuild' if you really mean it."
docker compose -f /volume1/docker/internal/compose.yaml \
--env-file /volume1/docker/internal/.env.prod \
up -d --force-recreate --no-deps {{safe_services}}
db-rebuild:
@echo ""
@echo " WARNING: you are about to rebuild the MSSQL container."
@echo " This will wipe the volume if it is not externally mounted."
@echo ""
@echo " Type the database name to continue: {{db_name}}"
@read -p " > " confirm && [ "$confirm" = "{{db_name}}" ] || (echo " Aborted." && exit 1)
docker compose -f /volume1/docker/internal/compose.yaml up -d --force-recreate db
The deploy recipe lists exactly which containers are safe to recreate. MSSQL is not in the list, so a fresh just deploy cannot touch it, no matter how tired I am or how badly I want to “just rebuild everything” at 1am. The db-rebuild recipe exists for the rare case where I do need to rebuild the database container, and it forces me to type the database name out loud before doing it. By the time I have typed internal_prod into the terminal, I have stopped and asked myself if I am sure twice.
The goal of that recipe is not to make the dangerous command impossible. It is to make it impossible to do by accident. A two-second prompt is enough friction to turn a misclick into a pause, and a pause is all you need to catch yourself. The first time I ran just db-rebuild for a real reason, I read my own warning text and aborted. The migration plan I had in my head was not actually finished. The recipe caught it. A fresh terminal and an arrow-up would not have.
Stop typing deploy commands from memory
Add a justfile to a project before the first deploy command leaves your terminal. Write the recipe out, name it, comment what it does and what it does not do. The five minutes you spend doing that is the cheapest insurance you will ever buy against your own muscle memory. The next person to touch the project, future-you very much included, will not have to read your shell history to figure out which arrow-up is safe to press.
If you want the full breakdown of what justfile can do, read the docs here. Fair warning, you will spend a Saturday rewriting your shell aliases.