DEV Community

Menshikov Vasil
Menshikov Vasil

Posted on

The brew services Setup That Finally Got Docker Desktop Off My Mac (and Made Local Postgres Feel Instant)

For a long time the first fifteen minutes of my workday belonged to a whale. I'd open the laptop, hear the fans spin up, watch "Docker Desktop is updating," and wait - all so I could run a single local Postgres container that my Mac was perfectly capable of running on its own. The morning three people on my team showed up late to standup for the exact same reason ("my fans won't stop and Postgres won't bind," "I ran docker compose up and my battery's already at 40%"), something in me finally snapped. This is the brew services setup that got Docker Desktop off our machines, and honestly, made local development feel joyful again.

Why this bugged me for years

Our local stack was not exotic. One docker-compose.yml with Postgres, Redis, and MySQL. That's it. And yet Docker Desktop's VM taxed every machine the entire day - idle RAM I could feel, CPU burned on file-sync I never asked for, a licensing question hanging over the whole thing, and that update nag that always seemed to block me at the worst possible moment.

The licensing part had teeth, too. Under the Docker Subscription Service Agreement, Docker Desktop needs a paid subscription once your company is big enough - the free tier caps out under 250 employees and $10M revenue. We were over one of those lines, which meant Docker Desktop wasn't only a performance tax, it was a per-seat bill for the privilege of running one Postgres. That combination gnawed at me for a genuinely embarrassing amount of time before I did anything about it.

What finally reframed it was saying the obvious thing out loud: on a Mac, you already own a first-class service supervisor. It's called launchd, it's what Apple uses to run its own daemons, and Homebrew speaks it fluently. I'd been renting a Linux VM to babysit a database my operating system already knew how to babysit.

Why brew services instead of a container

The pitch is almost too simple. Homebrew installs Postgres, Redis, MySQL, MongoDB, Nginx, RabbitMQ, Elasticsearch, Kafka - the whole cast of local dev dependencies - as native formulae. Then brew services wraps macOS's own launchd so those processes start at login, get supervised, and stop cleanly (the full command surface lives in the Homebrew manpage). No VM. No file-sync layer. No 4GB toll booth in front of a single database.

I want to be honest about the trade-off, because I treated this as a decision and not a religion. Containers give you isolation, reproducibility, multiple versions side by side, and real production parity. Homebrew gives you native performance, near-zero overhead, and a Mac-first experience for the ninety-percent case where you just need a Postgres to develop against. Both of those are true at once. If you want the longer version of that decision, with all the commands and the honest tradeoffs laid out, someone wrote up the whole Homebrew-vs-containers case here and it's a good companion read.

And yes, I did the responsible thing and evaluated the usual Docker Desktop alternatives first. Colima, OrbStack, and Podman are all genuinely lighter than Docker Desktop and worth knowing - Colima especially gives you a Docker-compatible runtime with minimal fuss. But they're still containers. They shave the licensing and some of the RAM; they don't change the fundamental fact that you're running a Linux VM to host a database your Mac can run natively. For a single local Postgres or Redis, brew services isn't a lighter container runtime. It's no container at all - which is the part most "Docker Desktop alternatives 2025" roundups quietly skip.

The setup: four commands and a launchd file

Here's the entire day-to-day surface. Four commands:

# Start a service
brew services start postgresql

# Stop a service
brew services stop postgresql

# Restart a service
brew services restart postgresql

# List all services and their status
brew services list
Enter fullscreen mode Exit fullscreen mode

That last one is the command I actually live in. It hands you an at-a-glance view of what's running and where its config lives:

$ brew services list
Name       Status  User    Plist
mysql      started username /Users/username/Library/LaunchAgents/homebrew.mxcl.mysql.plist
postgresql stopped
redis      started username /Users/username/Library/LaunchAgents/homebrew.mxcl.redis.plist
Enter fullscreen mode Exit fullscreen mode

Our old onboarding doc had a two-page section titled "Installing and troubleshooting Docker Desktop." The new version is a shell snippet:

brew install postgresql@16 redis mysql
brew services start postgresql@16
brew services start redis
brew services start mysql
Enter fullscreen mode Exit fullscreen mode

A new hire now goes from a fresh laptop to a working database stack in about the time it takes to fetch coffee. No account, no login, no license-acceptance dialog. The first time I watched someone new run that snippet and just... have a database, I felt a little pang about all the mornings I'd lost to the whale.

What's actually happening underneath

I didn't want to hand anyone a magic command they couldn't reason about, so we documented the mechanism. When you run brew services start postgresql, Homebrew generates and registers a .plist with launchd - the same supervision layer Apple uses for system daemons. Apple's Technical Note TN2083 on Daemons and Agents is the canonical reference for how login agents like this get loaded, and thoughtbot's walkthrough of starting and stopping services with Homebrew is a friendly plain-English companion. The generated plist is refreshingly readable:

<?xml version="1.0" encoding="UTF-8"?>
<plist version="1.0">
<dict>
  <key>Label</key>
  <string>homebrew.mxcl.postgresql</string>
  <key>ProgramArguments</key>
  <array>
    <string>/usr/local/opt/postgresql/bin/postgres</string>
    <string>-D</string>
    <string>/usr/local/var/postgres</string>
  </array>
  <key>RunAtLoad</key>
  <true/>
  <key>KeepAlive</key>
  <true/>
  <key>StandardErrorPath</key>
  <string>/usr/local/var/log/postgres.log</string>
</dict>
</plist>
Enter fullscreen mode Exit fullscreen mode

RunAtLoad starts it at login. KeepAlive restarts it if it dies. Logs land at a predictable path you can just tail. Once people saw that this was only launchd doing what it already does for the whole OS, the "but is it reliable?" worries quietly dissolved.

How it feels now

I'm not going to pretend "trust me, it feels faster" is data, so we did measure it across a handful of volunteer machines for a couple of weeks. The headline is the memory. The local database stack went from something like 4GB of idle RAM under Docker Desktop to under 200MB with brew services - and getting roughly 4GB back on a 16GB MacBook is the difference between a laggy editor and a responsive one. Two people told me they'd stopped closing their browser to "make room" for the dev stack, which tells you everything about the world we'd been living in.

The rest followed. Cold database start dropped from nearly half a minute to under two seconds. The fans, which used to be a constant background hum, became a rare event. Onboarding went from around fourteen steps to four. And the annual license-management chore simply vanished. None of these numbers are exotic; they're just what happens when you stop running a VM to do a job your OS does natively.

The gotchas nobody puts in the README

It wasn't frictionless, and I'd be lying if I pretended otherwise. Version pinning is now on you - a container pins its version in the compose file, but Homebrew installs a version and a stray brew upgrade can move it out from under you mid-sprint. We standardized on the versioned formula, postgresql@16, and left a note in the Brewfile so nobody accidentally jumps a major version.

A service that "won't start" is almost always a stale socket or a log you haven't read yet. My debugging loop is boring and reliable: glance at brew services list, then actually read the log, then poke the service itself rather than trusting the status column.

brew services list
tail -f /usr/local/var/log/postgres.log
ps aux | grep postgres
launchctl list | grep postgres
Enter fullscreen mode Exit fullscreen mode

Nine times out of ten the log names the culprit outright - a leftover postmaster.pid, a port already bound, a data dir from an older major version.

The sudo brew services trap is worth calling out too. Running a service as your user (a login agent) versus as root (a system daemon) puts the plist in different directories and changes when it starts; TN2083 above explains exactly why. For local dev you almost always want the user-level agent. We deleted one sudo somebody had copy-pasted from a random blog and half of our "it doesn't start at boot" complaints evaporated. And finally: your data now lives on the host with no volume abstraction, which is simpler but means a careless brew uninstall can take your local data with it. Our rule became a one-liner - your local DB is disposable, seed it from a script, never keep anything you can't regenerate.

Where Docker still wins, and why I kept it

I did not declare war on Docker. I just retired Docker Desktop as the default local-database runtime. The instant you need several services wired together for an integration test, or byte-for-byte production parity, containers are still exactly right. Our whole policy collapsed into one sentence: Homebrew for quick local databases, Docker for multi-service integration tests. Both live in the repo; you reach for the lighter one by default. That single sentence ended months of "should we standardize on containers or not" bikeshedding, because it was never either/or - most jobs are just lighter than we'd assumed.

We're tightening a couple of loose ends now, mostly a checked-in Brewfile so brew bundle reproduces the exact service set - and because Homebrew Bundle can start services declaratively with restart_service: true, that closes most of the reproducibility gap containers otherwise own.

What stays with me, though, isn't the RAM graph. It's that my laptop is quiet in the morning now. There's a particular kind of craft in noticing the tool you've been tolerating for years and asking whether it's actually earning its keep - and if your machine sounds like a jet engine every morning to run one Postgres and a Redis, I'd gently suggest it isn't.

Sources & further reading

Top comments (0)