Have you ever asked someone else to help with your Rails app? Did you notice how painful it seemed for them to get everything working? Or how about contributing to someone else’s project? A bad setup experience can really suck away the motivation to help.
It usually goes something like this… you clone the repo from Github, look at the readme and realise it is still the default Rails template version. Ok, run bundle install, hopefully that goes well.
Next you try the database setup rake task but you’ve forgotten to put your details in the config file. Hopefully there’s a sample version to avoid the actual file being checked in to source control. If not, and you need to make a change, get ready for some annoying git stashing when it comes to pushing your code back.
What about those projects with a /db/migrate folder overflowing with migrations? Run rake db:migrate and hope they all still run?
Regardless of how much experience you have building apps with Rails, this is still a pain.
How did we end up like this?
Very often, the dependencies of your app evolve over time. You add a background worker to avoid a user having to wait for a process to complete, and then an A/B testing library or a feature flipper. Before too long there’s quite a list of dependencies that are not managed by any specific process.
The trouble is, tasks like creating and configuring these dependencies is often a one-time affair for you, well, once in your development environment and then once on your production server. Once done, it’s easy to forget that it’s a requirement for the app to work properly.
The solution then, is to automate as much of this as we can. And we can automate the majority, if not all of it.
The easiest way to do it is to keep a bash script up-to-date with the steps required to get the app up and running and let it evolve alongside the app’s requirements.
Generally this is what I start with in bin/setup:
echo "Installing dependencies"
bundle install --quiet
echo "Preparing database"
echo "All done. Run 'rails s' to start a server"
This will ensure my gem dependencies are up-to-date and my database is present and fully migrated. rake db:setup also runs the rake db:seed task, that populates the database from the db/seeds.rb code.
Make it idempotent
At this point, I’d like to recommend that you ensure your script can run in an idempotent fashion. Which is a succinct way of saying don’t overwrite things.
In order for this script to be useful and used by you, it shouldn’t overwrite or re-install things that are already up-to-date. Dependencies that haven’t changed don’t need to be re-installed and likewise, your db doesn’t need to be dropped and then created from scratch every time you run this script.
So, the script should aim to check what has been done so far and only perform steps that need to be run.
Fortunately, a lot of the system tools we use have this idea baked in. Bundler, for example, only updates your gems when a version has changed and you have asked for an update.
If you’re on a Mac, the brew command operates in a similar idempotent fashion.
As part of my setup script, I like to check for the presence of any dependencies that aren’t included in my app’s Gemfile. This generally is made up of databases and sometimes queue servers or a caching server.
Because we’re writing a shell script, we have all the power of the command line at our disposal and are able to make use of the which command for easy dependency checking.
if test ! $(which createdb)
echo "You need to install PostgreSQL. If you use Homebrew, run:"
echo " brew install postgresql"
echo "PostgreSQL found"
I repeat that block for any other dependencies the app has.
Another step we can automate is to create local versions of our standard .yml config files. At a minimum this will require us to create config/database.yml.
It’s good practice to keep configuration files that may differ between developer machines out of version control. So, when starting a new project I will create a config/database.yml.example file with generic values, this gets committed to version control, and then let the setup script create my local version, which gets ignored by my .gitignore settings.
if test -f config/database.yml
echo "Database config already set"
cp config/database.yml.example config/database.yml
Running the app with Foreman
Finally, the last common step we want is starting up all the services our app depends on. At the very least we want to fire up a web server. This is where we look to a great tool called Foreman.
Foreman allows us to define the processes our app needs in a Procfile and then manage these processes with a common set of commands, eg. foreman start and foreman stop.
We need to ensure that in the root folder of our app we create a Procfile:
web: bundle exec rails s
This is the simplest Procfile we need.
It can also include other entries such as worker: rabbitmq-server. You can do anything that you would at the command line, so feel free to add switches for port numbers for example.
In order to add this to our setup script, let’s output a message that instructs the user how to start the app.
echo "Success. The app is installed. To view it in a browser, run:"
echo " foreman start"
Summary / tl;dr
Automating your setup process will save time for you and anyone else who wants to contribute to your project.
Keep it current by updating it as your app changes.