Docker + Rails: Solutions to Common Hurdles

Tips n Tricks for working with Rails applications in Docker

This is the follow on post to an earlier post: Docker: Right for Us. Right for You?

Docker has made a number of large problems go away, and replaced them with (usually) (hopefully) a smaller set of its own problems. Sometimes it's not worth it to make that sacrifice, but it often is for us, and here are the tricks we've picked up for working with Rails sites and Docker Compose:

Rails Tips & Tricks

db host

Trying to docker-ify a Rails app and running into this?

could not connect to server: No such file or directory
  Is the server running locally and accepting
  connections on Unix domain socket "/var/run/postgresql/.s.PGSQL.5432"?

Make sure you have a db service defined in docker-compose.yml, and add host: db to your database.yml file. All of the services you define in the compose file become network-addressable names to each other. So adding host: db tells Rails that the hostname of the database server is db, which should point to your database service.


binding.pry

AKA the one tool I use the most by far I gotta have it. TLDR on the solution: https://stackoverflow.com/questions/35211638/how-to-debug-a-rails-app-in-docker-with-pry/37264588#37264588

The gist is that you need to add some Docker Compose flags to the service you want to run a pry inside, and then docker attach [running_container_name] after you've started the service container.


save_and_open_page

If you're testing w/ Capybara and want to get a glimpse of your site while a feature test is running, then Docker is going to make that difficult for you. TLDR - https://github.com/copiousfreetime/launchy/issues/108#issuecomment-319644326

And the gist? You need to configure a volume so screenshots saved on the Docker container are accessible to the host machine. You then need to rig up guard locally to watch the volume directory and automatically open any HTML files that land there.


cacheing bundler and npm builds

If you don't configure this properly, you could find yourself re-installing dependencies every time you spin up your containers. We lean on persisted volumes for this, and pick up the log and tmp directories while we're at it. like so:

- services
  - app:
    entrypoint: ./.docker-config/entrypoint.sh
    volumes:
      - ./node_modules:/usr/src/app/node_modules
      - gems:/usr/local/bundle
      - log:/usr/src/app/log
      - tmp:/usr/src/app/tmp

I'll also point out that we prefer to move the dependency installation steps into an entrypoint script, instead of within the Dockerfile build definition. This means that we're rarely rebuilding our baseline image (unless something deeper level changes, like needing an additional runtime available), and our dependencies are kept up to date everytime we cycle the containers. Here's an example of an entrypoint script for an app with Ruby gems and NPM packages:

#!/bin/bash

set -e

# Ensure PID is reset. This can happen if docker isn't cleanly shut down.
rm -rf /usr/src/app/tmp/pids

# Verify node_modules are up to date
yarn install --silent

# Verify gems are up to date
if ! bundle check > /dev/null; then
  echo "Gems dependencies are out of date. Installing..."
  bundle install
fi

exec "$@"

asset performance

Out of the box, sites that run perfectly quickly locally would take a lonnnnng time to respond to local asset requests. One solution we settled on was to add :delegated to the volume dedicated to syncing the codebase. For example:

app:
  image: ruby:3.0
  command: rails server -p 3000 -b '0.0.0.0'
  ports:
    - 3000:3000
  volumes:
    # Ensure changes to the codebase are picked up by the docker container
    # The `:delegated` here is a critical config for speedy development
    - .:/usr/src/app:delegated
  • The docs have a lot to say about this if you want to learn more: http://docs.docker.oeynet.com/docker-for-mac/osxfs-caching/
  • And this StackOverflow answer has a good recap in case that docs link goes down: https://stackoverflow.com/a/63437557/1655658

Running bash

This one took me a while to figure out as I was learning the difference between a running Docker container and a service and an image and a volume. Once I had a clearer picture of how Docker (and Docker Compose) worked, opening up a shell into a running thing stopped being tricky. Here's what I've picked up though, hopefully these words mean something to you:

If you have a running container based off of a docker-compose service, you can bash into it like so:

docker-compose exec [name-of-service] bash

If you have a non-running container based off of a docker-compose service, you can bash into it like so:

docker-compose run --rm [name-of-service] bash

The difference here is exec vs run --rm. You can probably guess what each does. The --rm flag is important so that the newly spun up containers don't hang around forever after you close out of the session.


importing database dumps

If you're running something like PostgreSQL within Docker, it becomes a bit trickier to psql your way in from a local command line. As is often the case with Docker, there are a few ways to get the job done here, and the one that's right for you is dependent on what you're trying to do.

One path is to requiring you to expose a port and run commands through that port. This can be done in a docker-compose.yml:

services:
  db:
    image: mdillon/postgis:11-alpine
    ports:
      # Expose Postgres to host networking (your computer) at port 5433.
      # This prevents conflicts with Postgres installed on your machine
      # while still allowing the database to be browsed at port 5432
      - 5433:5432
    volumes:
      - db-data:/var/lib/postgresql/data

Another option is to define a new volume within your database service which makes a database dump on your local machine accessible to the running Docker container. If you had a database dump stored within your project directory at ./local/db-backups, you could make it accessible within Docker like so:

services:
  db:
    image: mdillon/postgis:11-alpine
    volumes:
      - db-data:/var/lib/postgresql/data
      - ./local/db-backups:/app/db-backups

Then, bash into the running container and have at it with all your usual psql moves.

Note: I'm including the db-data volume definition to point out that we always have a persistent volume configured for database services so the data persists when the containers are wound down.


Using local SSH credentials on Docker container

We have a lot of deploy processes that lean on SSH private keys for authorization. If you attempt to run one of those commands in a running Docker container, it'll fail due to it's sandboxed environment not being able to reach out into your local machine for the credentials. This is our Docker Compose solution for that:

services:
  app:
    ...
    volumes:
      - type: bind
        source: /run/host-services/ssh-auth.sock
        target: /run/host-services/ssh-auth.sock

The End

That's about all I could recall now that it's been ~1.5 years since digging in on my own Docker journey. Hopefully something in there was useful to you. If you have another tip or trick that solved an early challenge, drop a line in the comments below!

Eli Fatsi

Eli uses his mathematics degree from Carnegie Mellon to blur the lines between the digital and physical worlds. He codes for Shure, Volunteers of America, and other clients from our Boulder, CO, office.

More articles by Eli

Sign up for The Viget Newsletter

Nobody likes popups, so we waited until now to recommend our newsletter, a curated periodical featuring thoughts, opinions, and tools for building a better digital world. Read the current issue.