READYFOR Tech Blog

READYFOR のエンジニアブログ

"Make"ing Docker commands with rails simplified

Hello and welcome, I am Steen - also known as Pensk - and today I will be talking about how I use Make to manage and automate my development environment and make docker a little easier to work with.

GNU make is a tool used for compiling and building large software projects, originally in C. By defining rules, you can combine and execute long complex shell commands, set environment variables and files, and even execute other rules defined in the file.

Make has a very intricate and complex set of syntax and behaviours, but today we will just scratch the surface to make handling docker, rails, and our database much more painless. So let's get started writing rules - in a file named Makefile. A make rule is defined by a name followed by a colon, and some shell commands you want it to execute on the next lines which must start with exactly one tab.

  
GITHUB_TOKEN := $(shell cat ~/.github/pat)

build:
    GITHUB_TOKEN=${GITHUB_TOKEN} docker-compose build

install:
    GITHUB_TOKEN=${GITHUB_TOKEN} docker-compose exec rails bundle install
    docker-compose exec rails npm ci
  

These two rules can be executed with make build and make install, respectively. The best thing is it just works, any shell on any system should have make ready to go. docker-compose commands can get really long, and for inexperienced software engineers knowing which to use in which order can be difficult to remember. By storing that in a Makefile it acts as documentation - the current commands to set up and run your software are committed along with it, so there's no need to go look up anything.

Let's talk about variables - both Make and environment. We defined GITHUB_TOKEN at the top of the file. This is a Github Personal Access Token which is used to authenticate to private repositories on the command line. Here we fetch it from the user's home directory and then pass it as an environment variable so docker can access and pull private code. Another big advantage of doing it this way is you can set the environment variable and then call make - the value will override the definition in the makefile.

Using that advantage, you can set GITHUB_TOKEN in your CI/CD pipelines and everything will just work. If you set the CI/CD pipelines to run a series of make commands - make build, install, migrate, test, etc. - Then when you update the make commands, the CI/CD pipeline will be up to date by default. With all the environment setup commands stored in one place your CI/CD pipelines will be more robust.

Make is also good for developer quality of life. We have a rake script for importing data into the local database to make developing with realistic data easier. Most engineers will only run it once, but it has a particular name and format. Something you would have to go and look for... until now.

  
IMPORT_DATE = $(shell date -v '-1d' +%F)

db-import:
    docker-compose exec rails bin/rails db_import:import[${IMPORT_DATE}]
  

With a little shell magic mixed in with make variables one only has to run make db-import and relax as all of their problems are automatically solved (all of their problems with development data, at least).

Most of our development machines here are macs, and as anyone who has tried developing rails on docker for mac knows - it's terribly slow. We handle the issue with mutagen, using it to sync our files to the docker container. It works well, but if not set up correctly you get an empty docker container, not very useful for development.

  
up:
    docker-compose up -d rails
    mutagen project start

down:
    -mutagen project terminate
    -docker-compose down

restart: down up
  

No more having to remember what commands or which order you need to get everything working. Make starts up docker, waits patiently for it to get running, and then tells mutagen to get to work. The dash in front of the commands for down tell make to ignore any errors and keep running commands - mutagen will complain if it is already stopped but that can safely be ignored. Restart shows how to run other rules. By declaring them as dependencies for restart, it will run down and then up.

Before we wrap this up, I will show some other examples of make rules that make daily development life a little easier.

  
db-create:
	docker-compose exec rails bin/rails db:create

db-create-test:
	docker-compose exec rails bin/rails db:create RAILS_ENV=test

migrate:
	docker-compose exec rails bin/rails db:migrate

migrate-test:
	docker-compose exec rails bin/rails db:migrate RAILS_ENV=test

seed:
	docker-compose exec rails bin/rails db:seed

run-spec:
	docker-compose exec rails bin/rails spec

rails:
	docker-compose exec rails bin/rails server -p 3000 -b 0.0.0.0

logs:
	docker-compose logs -f

shell:
	docker-compose exec rails /bin/bash

db-shell:
	docker-compose exec db mysql -proot

console:
	docker-compose exec rails bin/rails c
  

These allow for quick access to a shell in the rails container as well as mysql's cli, migrating and rspec tasks, as well as docker's logs and the rails console. Basically if I type anything at least twice I add it to make so I don't need to think about it again. I haven't had to type docker-compose in months!

Make is less intimidating and easier to read than shell scripts, but the advanced features are very deep and complex. There's no limit to the things you can accomplish, it is definitely not just a tool for compiling. I hope this short introduction has spurred some interest in Make, and you will try using it to document and automate common tasks in your development environment. Thank you for taking the time to read this article!

- pensk