Happy Ending: An Epic Saga of Guard and Docker
I’ve never liked how projects pollute my system. Libraries, databases, message queues, you name it and I’ve had to install it. Within this shared universe, it’s only a matter of time before worlds collide. Two projects each require a different Redis and I’m stuck being the human dependency resolver.
There have been past attempts at project silos. None of them have been as powerful or as promising as Docker. Even so, it’s not without its rough edges. This is a story about one of those edges.
I have a Rails project with existing tests that I wish to Dockerize. What I don’t have is a good way to get feedback as I refactor. I want to run Guard so I can do some TDD. Normally running Guard isn’t a big deal, but Docker (at least, on the Mac) makes this an issue (it has to do with libnotify). It’s difficult to impossible to get file changes to kick off your guards. New technologies come with trade-offs and sometimes the simple becomes difficult.
The First Duty
Gemfile is open and ready. I crack my knuckles, roll my shoulders, and begin to type:
group :test do ... gem 'guard-rspec' end
The Rails app I’m working on lives in a container named “web”. It interacts with a PostgreSQL database, a worker for background jobs, and a faux mail server. All orchestrated with docker-compose:
$ docker-compose run web bundle install
Text flies by as my system installs Guard. Now I need a
$ docker-compose run web bundle exec guard init
With that, everything should be ready:
$ docker-compose run web bundle exec guard
I’m greeted with a familiar prompt:
02:39:05 - INFO - Guard::RSpec is running 02:39:06 - INFO - Guard is now watching at '/app' Frame number: 0/0  guard(main)>
I strike the enter key and my tests begin to run. They complete with a series of satisfying green dots and I can’t help but smile. I open a model with vim and save. I look to Guard and…
Loud as a Whisper
Let’s back up for a moment. This development is happening in OSX. Docker doesn’t run on OSX. It runs on Linux and Linux alone. Running on OSX means using a small Linux VM conveniently packaged up as boot2docker.
I said that this development is happening in OSX. In truth, all of my work happens in OSX. It’s my home. It’s where I keep my dotfiles. I don’t want to shell into a VM to work. Thankfully, docker-compose understands my plight and will mount local directories into containers.
Where was I? Ah yes, I saved my model and tests should be running. They’re not.
Normally this would lead to a flurry of Google searches. There’s no need for that. I know what’s happened. Guard relies on file system notifications. Mounted volumes (in Virtual Box) change files under the cover of darkness and tell no one of what they’ve done. Guard waits silently, blissfully unaware of the changes.
There’s a full-proof solution for this. I scroll through the documentation looking for what I need.
$ docker-compose run web bundle exec guard --force-polling
guard is manually checking for changes to each file once a second. I switch back to my editor and save again. The model tests zip by and the fan in my laptop whirs to life.
Who Watches the Watchers
My fan is loud enough that I fear my computer may take flight. Polling files is more intense than I had expected.
Over 100% CPU? This’ll never work. If watching things is this expensive then I need to be careful with what I watch. I open the default
Guardfile I installed. It’s set up to watch everything I could ever need and it has the line count to prove it. I begin deleting lines in mass.
guard again and my fan still sounds like it’s giving all it’s got. It appears to be working though. In a moment of weakness I consider ear muffs from Amazon to stifle the noise. I shake the thought away.
A Matter of Time
I stare through my screen trying to think of a better way. A moment from my past surfaces what might be a solution. At a job I used to have we would deploy code with rsync. That’s almost the same problem. It might just work.
I immediately start to think of what needs to happen. I’ll have to get rsync installed in the Docker VM. Of course they release new images so it’ll have to be done each time. I’ll need an easy way to repeat the install. I can’t help but wonder what it’ll take to get it working in the containers on the VM. The truth is, I have no idea.
Operations has never been my strength and it’s showing. I do know of a solution that worked for me in the past. I’ll hope someone else has already done it.
I switch to Firefox and begin my search. I click through links, skim Stack Overflow answers, and land on a project called docker-osx-dev. It rsyncs local files to the VM and its maintainer, Yevgeniy Brikman, has already handled the tricky parts.
I install docker-osx-dev and hope it works as advertised. A wall of output shows files syncing and gives me a bit of confidence. I start
guard without the polling and save my model once more. My tests run. My fan is silent. My arms thrust up into the air. I love this part.
Fueled by triumph I press on. My tests are going but there’s a noticeable delay between runs. My gut says it’s Rails loading.
Delay from loading Rails is a well known problem with a well known solution. Rails now ships with Spring to solve this exact issue. It keeps a copy of Rails running in the background and then forks the process when needed.
I pop open my
Guardfile and add
spring to the command used to run
guard :rspec, cmd: "bundle exec spring rspec" do ... end
Confident in my fix I start up
03:32:35 - INFO - Running all specs Version: 1.3.6 Usage: spring COMMAND [ARGS] Commands for spring itself: binstub Generate spring based binstubs. Use --all to generate a binstub for all known commands. help Print available commands. status Show current status. stop Stop all spring processes for this project. Commands for your application: rails Run a rails command. The following sub commands will use spring: console, runner, generate, destroy, test. rake Runs the rake command
Crap. It looks like
spring has to know about the command and it’s not familiar with
rspec. There’s no way I’m the first person to do this. Back to Google.
Once again the community comes through with spring-commands-rspec. I add it to my
Gemfile, do another
bundle install, and
guard starts with no complaints. I save the model again and the tests run. Another save, another run, and the delay is gone.
All Good Things…
I take a deep breath and nod my head. The changes are committed with a small message that hardly represents the effort. I can’t help but think of those on who’s shoulders I am standing. Not of giants but of regular people. It’s a fairly dramatic thought considering I managed to get Guard running. It’s not as though I’ve saved the world. Still, I enjoy the moment and hope my shoulders manage to hold others. It’s late and I need to wind down before trying to sleep. After a trek like that I need something to watch. If only I could figure out what.