Dockerizing a Rails App Part 2: Beefing up Development

In Part 1 of this series, we saw how to get a complex Rails 5 app up and running in development using Docker. If you haven't started there, check it out. We have a few different goals in the section to help beef up our development environment.

Part 2 Goals:

The current build there works great for starter sites and MVP's but when you're working with a large code base with many files, Docker's volume system starts to bog down. The files take too long to get into your container grinds development time down to a turtle's pace.

Luckily, a kind soul named Eugen Mayer has built a quality tool called docker-sync to rectify this issue. Check it out here: http://docker-sync.io/

So our goals for this phase are to:

  • Speed up our volumes in a local development environment
  • Add handy bash wrapper scripts to make short work on running common commands
  • Add live reload so automatic reloading of assets and pages as we make changes

Now that we know what we're shooting for let' get started.

Setup example app:

Let's continue with the example app we started with in Part 1.

https://github.com/devato/docker-rails

To checkout Part 1:

$ git clone git@github.com:devato/docker-rails.git
$ cd docker-rails
$ git checkout tags/part-2 -b master

Adding docker-sync

Docker syncs headline read:

Run your application at full speed while syncing your code for development, finally empowering you to utilize docker for development under OSX/Windows/*Linux

That's precisely what we need, so to get started install the gem:

$ gem install docker-sync
$ docker-sync-stack --version # should give you a version 

There are a few different options for syncing tools, but let's go with rsync, which is a fast, versatile, remote (and local) file-copying tool.

TIP: All this configuration may start to feel cumbersome, but this effort will pay off once we have a solid development environment.

Adding a docker-sync.yml file is next. This file works similarly to a docker-compose.yml file but it interacts with docker and docker-compose for us so we don't have to run different sets of commands.

Here's the file:

# ./docker-sync.yml
version: "2"
syncs:
  dockerapp-sync:
    sync_strategy: 'rsync'
    src: './'
    sync_host_port: 10872
    sync_args: '--inplace'
    notify_terminal: false
    #watch_args: '-v'

Because docker-sync will change the way our code is handled in the volume for web, we'll need to update the docker-compose.yml file too.

This is the updated web service with a new volume:

version: '3.3'

services:
  web: &web_base
    depends_on:
      - 'postgres'
      - 'redis'
    build: .
    entrypoint: './docker/entrypoint.sh'
    command: bundle exec puma -C config/puma.rb
    ports:
      - '3000:3000'
      - '35729:35729'
    volumes:
      - 'bundle_cache:/bundle'
      - 'dockerapp-sync:/dockerapp:nocopy'
      - '.:/dockerapp'
    env_file:
      - '.env'
    stdin_open: true
    tty: true

  postgres:
    image: 'postgres:9.6-alpine'
    environment:
      POSTGRES_USER: 'postgres'
      POSTGRES_PASSWORD: 'supersecret'
    ports:
      - '5432:5432'
    volumes:
      - 'postgres:/var/lib/postgresql/data'

  redis:
    image: 'redis:3.2-alpine'
    command: redis-server --requirepass supersecret
    ports:
      - '6379:6379'
    volumes:
      - 'redis:/data'

  sidekiq:
    <<: *web_base
    command: bundle exec sidekiq
    ports: []
    depends_on:
      - 'web'
    env_file:
      - '.env'

volumes:
  redis:
  postgres:
  bundle_cache:
  dockerapp-sync:
    external: true

NOTE: We used dockerapp here, but you need to make each app's sync volume name unique. Otherwise unexpected code will end up in the volume. The convention is to end the volume name with sync so they can be distinguished from normal volumes.

With this in place we should be ready to start the stack up. Docker sync gives you a different set of commands to start and stop servers.

Let's start off by rebuilding the image, then we can start it up using docker-sync:

$ docker-compose build --no-cache # rebuild images
$ docker-sync-stack start

Docker sync should be up and running, and it will push changes to your local files into your Docker containers instantly.

Add wrapper scripts

Let's add some wrapper scripts around these new commands to make our builds and startup process much easier:

  1. Add a bin/server file:
#!/bin/bash
        
docker-sync-stack clean
docker-sync-stack start 

  1. And make it executable:
$ chmod +x bin/server

Running bin/server will stop and clean up any volumes or running processes, and then start the server afresh.

Using these simple wrapper scripts we can make server startup task etc more mangageable. You can expand on this approach and automate many different tasks like setting up the Db, importing data and seeding.

Add guard-livereload

First make sure your server is not running.

  1. Add guard and rack livereload gems to the development group:
  gem 'guard-livereload', '~> 2.5', require: false
  gem 'rack-livereload'
  1. Add the config to config/environments/development.rb:
  logger           = ActiveSupport::Logger.new(STDOUT)
  config.middleware.use(Rack::LiveReload, {
    live_reload_port: ENV['RACK_RELOAD_PORT']  # default 35729
  })
  1. Add RACK_RELOAD_PORT to the .env file:
RACK_RELOAD_PORT=35729
  1. Finally we need to open this port on the app service in docker-compose.yml:
...
   ports:                     
     - '3000:3000'            
     - '35729:35729'
...

You may need to rebuild the containers after opening up the ports:

$ docker-compose build --no-cache

Run bin/server to start the server up, and it will detect the gems that aren't installed and handle it for you.

(NOTE: You may need to install guard-livereload locally to initialize a Guardfile, which has many different coniguration options but we're not going to cover them here).

You can use the default Guardfile by running:

$ guard init livereload

Now you're ready to start your server and watch your changes happen in the browser.

$ bin/server

Wrapping up

Let's review our Goals for this section:

  • Speed up our volumes in a local development environment
  • Add handy bash wrapper scripts to make short work on running common commands
  • Add live reload so automatic reloading of assets and pages as we make changes

There are many different ways to set up a Rails development environment and these are just a few strategies to increase stability and efficiency.

If you find any issues with this guide, or run into any problems, feel free to post in the comments or reach out to me on twitter: @tmartin8080

In our upcoming guide, we are going to cover how to set up a smooth testing environment. Let us know if you have any suggestions for things to cover.

Happy Whaling!