Setup Rails 7.2 and Docker Compose for Capybara/Selenium/RSpec and more: Step-by-Step guide

I’ll show you how to set up a complete development environment for Rails 7.2 using Docker and Docker Compose. Whether you’re just getting started with Docker or you’ve been using for a while, this guide will have you up and running in no time. We will cover:

  • Rails 7.2 with Docker and Docker Compose Setup
  • Step-by-Step guide on How to Setup a New Rails Project
    • Postgres configuration
    • Rspec configuration
    • Sidekiq configuration
    • Selenium and Capybara Configuration
  • Testing the Setup – Run it in your machine and see the tests running inside the selenium docker image

You can get the project and run the examples here: https://github.com/viniciusoyama/rails-7.2-docker-compose-setup-example

Rails 7.2 + Docker Compose Setup

My docker compose setup uses the following files:

  • Dockerfile – Base image for running the Rails app
  • docker-compose.yml – Describes services (web, sidekiq, postgres, redis) and volumes for the databases and for caching the gems
  • docker-compose.test.yml – Describing services used in tests (Selenium and some changes on the Web)
  • docker-compose.override.yml – It’s a file that is not versioned by git. You can use it to modify something for your machine like the selenium image for ARM processors
  • run file – Utility file that simplifies our workflow when we need to run some docker compose commands

Dockerfile Setup

Dockerfile


FROM ruby:3.1.2-slim

RUN apt-get update -qq && apt-get install -yq --no-install-recommends \
    build-essential \
    gnupg2 \
    less \
    git \
    libpq-dev \
    postgresql-client \
  && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*

ENV LANG=C.UTF-8 \
  BUNDLE_JOBS=4 \
  BUNDLE_RETRY=3

RUN gem update --system && gem install bundler

WORKDIR /usr/src/app

ENTRYPOINT ["./docker-entrypoint-web.sh"]

EXPOSE 3000

CMD ["bundle", "exec", "rails", "s", "-b", "0.0.0.0"]

docker-entrypoint-web.sh


#!/bin/bash
set -e

# Remove a potentially pre-existing server.pid for Rails.
rm -f /usr/src/app/tmp/pids/server.pid

echo "bundle install..."
bundle check || bundle install --jobs 4

# Then exec the container's main process (what's set as CMD in the Dockerfile).
exec "$@"

Some notes explaining this Dockerfile:

  • We’re using the ruby:3.1.2-slim image. It’s the smaller version of the official Ruby image and should work fine.
  • The RUN command installs essential packages required for compiling Ruby gems and interacting with the PostgreSQL database. You can add more things here.
  • Environment Variables:
    • LANG=C.UTF-8: Sets the default locale to UTF-8 to ensuring proper encoding.
    • BUNDLE_JOBS=4 and BUNDLE_RETRY=3: Speeds up the bundling process by using multiple cores and retries on failure.
  • ENTRYPOINT ["./docker-entrypoint-web.sh"]: sets a custom entrypoint script to run before running any other command. This way we can cache our gems and avoid installing everything again every time we add/remove a gem.

Docker Compose Setup

The docker-compose.yml file

services:
  web:
    build: .
    command: bash -c "rm -f tmp/pids/server.pid && bin/rails s -p ${RAILS_HOST_PORT} -b '0.0.0.0'"
    volumes:
      - .:/usr/src/app
      - bundle:/usr/local/bundle
    ports:
      - "${RAILS_HOST_PORT}:${RAILS_HOST_PORT}"
    env_file:
      - "./.env"
    tty: true
    stdin_open: true
    depends_on:
      - postgres
  sidekiq:
    build: .
    volumes:
      - .:/usr/src/app    
    tty: true
    env_file:
      - "./.env"
    command: "bundle exec sidekiq -C config/sidekiq.yml"
  postgres:
    image: postgres:14
    ports:
      - "5432:5432"
    env_file:
      - .env
    volumes:
      - pg_data:/var/lib/postgresql/data
  redis:
    image: "redis:7-alpine"
    ports:
      - 6379
    volumes:
    - redis_data:/var/lib/redis/data
volumes:
  pg_data:
  bundle:
  redis_data:

This file is the primary configuration for Docker Compose: it defines all services and their configurations. Here are some notes:

  • web:
    • Builds the Docker image from the Dockerfile and runs the Rails server. It mounts the local code into the container for live reloading during development.
    • The command parameter ensures that any leftover server process IDs are removed before starting the Rails server. This prevents issues with server restarts.
    • It exposes the port specified in the .env file for external access.
    • We are mounting a bundle volume in order to cache gems
  • sidekiq:
    • The sidekiq service is responsible for background job processing. It shares the code volume with the web service, ensuring it has access to the same codebase.
    • It runs using the config/sidekiq.yml configuration file.
  • postgres:
    • The pg_data volume ensures that database data is persisted across container restarts.
  • redis:
    • Persists data using the redis_data volume.

      The docker-compose.test.yml file


services:
  web:
    environment:
      - RAILS_ENV=test
      - SELENIUM_REMOTE_HOST=selenium
      - SELENIUM_REMOTE_PORT=4444

    depends_on:
      - selenium

    command: sleep infinity

  selenium:
    image: selenium/standalone-chrome

    environment:
      - SE_VNC_NO_PASSWORD=true
      - VNC_NO_PASSWORD=true
      - SCREEN_WIDTH=1500 
      - SCREEN_HEIGHT=900
    ports:
      - 4444:4444
      - 5900:5900
      - 7900:7900

This file is an used as an extension of the main docker-compose.yml. It adds configurations specific to the test environment.

This one is the most interesting one. As we are running inside a docker container, in order to run integration tests/specs using a real browser we use a dedicated selenium image with chrome and setup capybara to properly connect to the selenium service (explained later in the article).

  • web:
    • Overrides the RAILS_ENV environment variable to test and sets the SELENIUM_REMOTE_HOST to selenium, pointing to the selenium service for running Capybara tests with Selenium.
    • The command: sleep infinity line is a workaround to keep the web service running during tests, allowing Selenium to connect to it.
  • selenium:
    • Uses the selenium/standalone-chrome image to provide a browser environment for integration tests.
    • The SE_VNC_NO_PASSWORD and other environment variables configure the Selenium container to run without a VNC password and set the screen resolution, which can be useful for debugging visual tests.
    • Ports 4444, 5900, and 7900 are exposed for interacting with the Selenium server and VNC.

This configuration allows you to run integration tests with a real browser, providing a robust test environment.

The docker-compose.override.yml file:


services:
  selenium:
    image: seleniarm/standalone-chromium:latest

This file is used to make local modifications to the setup that are not tracked by version control. It allows individual developers to customize their development environment without affecting others or commiting to the repository.
In this example I’m changing the selenium image to be the one compiled for arm . This is necessary in order to run the specs in my Macbook with an Arm processor.

The run file

#!/usr/bin/env bash

set -o errexit
set -o pipefail
set -o nounset

DC="${DC:-exec}"

# If we're running in CI we need to disable TTY allocation for docker compose
# commands that enable it by default, such as exec and run.
TTY=""
if [[ ! -t 1 ]]; then
  TTY="-T"
fi

# -----------------------------------------------------------------------------
# Helper functions start with _ and aren't listed in this script's help menu.
# -----------------------------------------------------------------------------

function _dc {
  docker compose "${DC}" ${TTY} "${@}"
}

function _build_run_down {
  docker compose build
  docker compose run ${TTY} "${@}"
  docker compose down
}

# -----------------------------------------------------------------------------

function cmd {
  # Run any command you want in the web container
  _dc web "${@}"
}

function rails {
  # Run any Rails commands
  cmd rails "${@}"
}

function rspec {
  docker compose \
  -f docker-compose.yml \
  -f docker-compose.test.yml \
  -f docker-compose.override.yml \
  run -e "RAILS_ENV=test" --rm web bundle exec rspec "${@}"
}

function bash {
  ## Start a Bash session in the web container
  cmd bash "${@}"
}

function psql {
  ## Connect to PostgreSQL with psql
  # shellcheck disable=SC1091
  . .env
 _dc postgres psql -U "${POSTGRES_USER}" "${@}"
}

function redis-cli {
  ## Connect to Redis with redis-cli
  _dc redis redis-cli "${@}"
}

function bundle:install {
  ## Install Ruby dependencies and write out a lock file
  _build_run_down web bundle install
}

function bundle:outdated {
  ## List any installed gems that are outdated
  cmd bundle outdated
}

function bundle:update {
  ## Update any installed gems that are outdated
  cmd bundle update
  bundle:install
}

function help {
  printf "%s <task> [args]\n\nTasks:\n" "${0}"

  compgen -A function | grep -v "^_" | cat -n

  printf "\nExtended help:\n  Each task has comments for general usage\n"
}

# This idea is heavily inspired by: https://github.com/adriancooney/Taskfile
TIMEFORMAT=$'\nTask completed in %3lR'
time "${@:-help}"

I’ve got this script from here: https://github.com/nickjj/docker-rails-example/blob/main/run

And made some changes. This shell script provides a convenient way to interact with the Docker Compose setup, encapsulating common tasks and commands.

The file should be self explanatory but here are some highlights:

  • Common Commands:
    • cmd: Runs any command inside the web container.
    • rails: Runs Rails commands, e.g., migrations or starting the console.
    • rspec: Runs the test suite with the appropriate Docker Compose configuration for the test environment. Note that it also includes the docker-compose.test.yml file.
    • bash: Opens a Bash shell in the web container.
    • psql: Connects to the PostgreSQL database using the credentials from the .env file.
    • redis-cli: Opens a Redis CLI session.
  • Bundler Commands:
    • bundle:install, bundle:outdated, bundle:update: Simplifies common Bundler tasks like installing, checking for outdated gems, and updating dependencies.

Now that we’ve understood the files related to Docker and Docker compose, let’s go and configure a new Rails project.

Rails and Docker: Step-by-Step guide on How to Setup a New Project

If you want to setup a new project by yourself, you can clone the the project and change it to the initial commit containing only the files:

  • Dockerfile
  • docker-compose.override.yml
  • docker-compose.test.yml
  • docker-compose.yml
  • docker-entrypoint-web.sh
  • run

Clone the project : https://github.com/viniciusoyama/rails-7.2-docker-compose-setup-example

Go to the first commit:

git checkout 7e0fff9ebf164de3ed67923151aea39cab154324

Setup Rails with Postgres

We can generate a new Rails project from scratch by following these steps:

In the terminal run:

  • run docker compose build
  • run docker compose run web bash to enter the web container
    • Depending on how you’ve cloned the project, you may need to executed chmod +x docker-entrypoint-web.sh to make the file executable

Now, inside the container run:

  • gem install rails -v 7.2
  • rails new . --name=my-app -T -d postgresql
    • This will install create a new Rails project with Postgres and skip tests (we will use RSPEC)
    • You can allow override of the Gemfile but skip the Dockerfile override
  • Exit the container by typing exit

Edit the config/database.yml with:


default: &default
  adapter: "postgresql"
  encoding: "unicode"
  database: "<%= ENV.fetch("POSTGRES_DB") { "my_project" } %>"
  username: "<%= ENV.fetch("POSTGRES_USER") { "my_project" } %>"
  password: "<%= ENV.fetch("POSTGRES_PASSWORD") { "password" } %>"
  host: "<%= ENV.fetch("POSTGRES_HOST") { "changeme" } %>"
  port: "<%= ENV.fetch("POSTGRES_PORT") { 5432 } %>"
  # http://guides.rubyonrails.org/configuring.html#database-pooling
  pool: "<%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>"

development:
  <<: *default
  database: <%= ENV.fetch("POSTGRES_DB") { "my_project" } %>_development

test:
  <<: *default
  database: <%= ENV.fetch("POSTGRES_DB") { "my_project" } %>_test

production:
  <<: *default
  database: <%= ENV.fetch("POSTGRES_DB") { "my_project" } %>_production

and edit your .env to have the following variables:


POSTGRES_USER=postgres
POSTGRES_PASSWORD=changeme
POSTGRES_DB=my_app
POSTGRES_HOST=postgres
POSTGRES_PORT=5432

Let’s create the database by running in the terminal:

  • docker compose up web
  • ./run rails db:create – Run it in another tab

Now you should be able to access your server at http://localhost:3000/

Setup Rspec

Stop the docker web and add to your Gemfile:

group :development, :test do
  gem 'rspec-rails'
end

Execute in terminal:

  • docker compose up web – to install the rspec gem inside the container
  • ./run rails generate rspec:install – to install rspec files

Setup Sidekiq

Stop the docker web and add to your Gemfile:


gem "sidekiq"

Add a config/sidekiq.yml file:


:concurrency: <%= ENV['SIDEKIQ_WORKERS'] || 4 %>
:timeout: 3600
:queues:
  - [default]

Add the config/initializers/sidekiq.rb file:


Sidekiq.configure_server do |config|
  config.redis = { url: ENV["REDIS_URL"] }
end

Sidekiq.configure_client do |config|
  config.redis = { url: ENV["REDIS_URL"] }
end

Edit the .env with:


# Sidekiq configuration
SIDEKIQ_WORKERS=4
REDIS_URL="redis://redis:6379"

Execute in terminal:

  • docker compose up – now all services should be running and working

Setup Selenium and Capybara

As the cromium browser running inside the selenium container is older than the modern ones supported by default on Rails 7.2, we need to change our ApplicationController to allow older browsers:

class ApplicationController < ActionController::Base
  # Only allow modern browsers supporting webp images, web push, badges, import maps, CSS nesting, and CSS :has.
  allow_browser versions: { edge: 88, chrome: 90, opera: 75 }
end

Stop the containers and add to the Gemfile

group :test do
  gem 'capybara', '~> 3.40'
  gem 'database_cleaner'
  gem 'rspec-rails'
  gem 'selenium-webdriver', '4.22'
end

Edit the spec/rails_helper.rb to include files from spec/support folder by uncommenting the line and remove the config.use_transactional_fixtures = true:


Rails.root.glob('spec/support/**/*.rb').sort_by(&:to_s).each { |f| require f }

Create the spec/support/database_cleaner.rb file:

RSpec.configure do |config|
  config.use_transactional_fixtures = false

  config.before(:suite) do
    raise(<<-MSG) if config.use_transactional_fixtures?
          Delete line `config.use_transactional_fixtures = true` from rails_helper.rb
          (or set it to false) to prevent uncommitted transactions being used in
          JavaScript-dependent specs.

          During testing, the app-under-test that the browser driver connects to
          uses a different database connection to the database connection used by
          the spec. The app's database connection would not be able to access
          uncommitted transaction data setup over the spec's database connection.
        MSG
    DatabaseCleaner.clean_with(:truncation)
  end

  config.before(:each) { DatabaseCleaner.strategy = :transaction }

  config.before(:each, type: :feature) do
    # :rack_test driver's Rack app under test shares database connection
    # with the specs, so continue to use transaction strategy for speed.
    driver_shares_db_connection_with_specs =
      Capybara.current_driver == :rack_test

    unless driver_shares_db_connection_with_specs
      # Driver is probably for an external browser with an app
      # under test that does *not* share a database connection with the
      # specs, so use truncation strategy.
      DatabaseCleaner.strategy = :truncation
    end
  end

  config.before(:each) { DatabaseCleaner.start }

  config.append_after(:each) { DatabaseCleaner.clean }
end

Create the spec/support/capybara.rb file:

require 'capybara/rails'
require "capybara/rspec"
require 'selenium-webdriver'

Capybara.server_host = '0.0.0.0'
Capybara.server_port = '3001'

Capybara.register_driver :remote_selenium do |app|
  options   = Selenium::WebDriver::Chrome::Options.new
  options.add_argument("--window-size=1320,830")
  options.add_argument("--no-sandbox")
  options.add_argument("--disable-dev-shm-usage")
  selenium_url = "http://#{ENV['SELENIUM_REMOTE_HOST']}:4444/wd/hub"

  Capybara::Selenium::Driver.new(
    app,
    browser: :remote,
    url: selenium_url,
    options: options,
  )
end

Capybara.configure do |config|

  config.default_driver = :remote_selenium
  config.javascript_driver = :remote_selenium
  config.always_include_port = true

  config.app_host = "http://#{IPSocket.getaddress(Socket.gethostname)}"
end

Stop the containers and run:

  • docker compose up

You now ran run the tests with ./run rspec spec.

Let’s break down each part of this capybara configuration:

Setting the Server Host and Port


Capybara.server_host = '0.0.0.0'
Capybara.server_port = '3001'
  • Capybara.server_host = '0.0.0.0': This configuration makes Capybara’s internal web server accessible on all network interfaces, which is necessary when running tests inside a Docker container. It allows the Selenium service to connect to the Capybara server from outside the container.

  • Capybara.server_port = '3001': Specifies the port that Capybara will use to run the test server. This must be different from the default Rails port to avoid potential conflicts. In this setup, port 3001 is used, while Rails dev server is running on port 3000 by default.

Registering the Remote Selenium Driver


Capybara.register_driver :remote_selenium do |app|
  options  = Selenium::WebDriver::Chrome::Options.new
  options.add_argument("--window-size=1320,830")
  options.add_argument("--no-sandbox")
  options.add_argument("--disable-dev-shm-usage")
  selenium_url = "http://#{ENV['SELENIUM_REMOTE_HOST']}:4444/wd/hub"

  Capybara::Selenium::Driver.new(
    app,
    browser: :remote,
    url: selenium_url,
    options: options,
  )
end
  • This part register a custom Capybara driver named :remote_selenium that uses a remote Selenium server.
  • --no-sandbox: Prevents issues with running Chrome in a containerized environment where the sandboxing features may not be supported or could cause permission problems.
  • --disable-dev-shm-usage: Avoids crashes due to the limited shared memory available in Docker containers by directing Chrome to use the regular filesystem for temporary storage.
  • The SELENIUM_REMOTE_HOST and the SELENIUM_REMOTE_PORT environment variables are set by the docker-compose.test.yml file.

Configuring Capybara Defaults


Capybara.configure do |config|
  config.default_driver = :remote_selenium
  config.javascript_driver = :remote_selenium
  config.always_include_port = true
  # This line ensures that selenium can connect to the test web server
  # if we use the web docker compose service hostname doesn't work
  config.app_host = "http://#{IPSocket.getaddress(Socket.gethostname)}"
end
  • config.default_driver = :remote_selenium and config.javascript_driver = :remote_selenium: Sets the default driver for all tests and for tests involving JavaScript to :remote_selenium. This ensures that all tests run using the remote Selenium server.
  • config.always_include_port = true: Ensures that the port is always included in URLs sent by Capybara to the selenium service. This is necessary to avoid connection issues between the Selenium Server and the test Web Server.
  • config.app_host = "http://#{IPSocket.getaddress(Socket.gethostname)}": Sets the app host for Capybara to the IP address of the Docker container. This ensures that Capybara and Selenium can communicate with each other using the correct IP and port. I’ve tried a lot of thinks with docker/docker compose networks in order to be able to use a hostname like "http://web-test" but couldn’t make it work.

    Testing the Setup

In order to test it, let’s create a new CRUD for users:

  • ./run rails g scaffold Users name:string
  • ./run rails db:migrate

And create a feature spec test in spec/features/managing_users_spec.rb


require 'rails_helper'

context 'Managing users' do 
  specify 'I can list users' do
    User.create(name: "Pedro")
    User.create(name: "Paula")

    visit users_path

    expect(page).to have_content("Pedro")
    expect(page).to have_content("Paula")
  end

  specify 'I add a user' do

    visit users_path

    click_link "New user"

    fill_in "Name", with: "New user"
    click_button "Create User"

    expect(page).to have_content("User was successfully created.")

    expect(User.last.name).to eq("New user")
  end
end

Run the spec with:

./run rspec spec/features --format documentation

You should see:


Managing users
  I can list users
  I add a user

Finished in 0.15137 seconds (files took 2.07 seconds to load)
2 examples, 0 failures

You can also see the specs running by going to http://localhost:7900/ , clicking on connect and running the specs again: ./run rspec spec/features --format documentation

Wrapping It All Up

Congrats, you made it to the end! 🥳

By now, you should have a shiny new Rails 7.2 development environment running inside Docker complete with all the usual libraries: PostgreSQL, Sidekiq, Selenium, and Capybara.

So, what’s next?

Thanks for reading to the and!



Subscribe to my newsletter!

I share content about Software Development & Architecture, Entrepreneurship and Lifelong Learning

Scroll to Top