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 appdocker-compose.yml– Describes services (web, sidekiq, postgres, redis) and volumes for the databases and for caching the gemsdocker-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 processorsrunfile – 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-slimimage. It’s the smaller version of the official Ruby image and should work fine. - The
RUNcommand 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=4andBUNDLE_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
Dockerfileand runs the Rails server. It mounts the local code into the container for live reloading during development. - The
commandparameter 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
.envfile for external access. - We are mounting a
bundlevolume in order to cache gems
- Builds the Docker image from the
- sidekiq:
- The
sidekiqservice is responsible for background job processing. It shares the code volume with thewebservice, ensuring it has access to the same codebase. - It runs using the
config/sidekiq.ymlconfiguration file.
- The
- postgres:
- The
pg_datavolume ensures that database data is persisted across container restarts.
- The
- redis:
- Persists data using the
redis_datavolume.
The
docker-compose.test.ymlfile
- Persists data using the
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_ENVenvironment variable totestand sets theSELENIUM_REMOTE_HOSTtoselenium, pointing to theseleniumservice for running Capybara tests with Selenium. - The
command: sleep infinityline is a workaround to keep the web service running during tests, allowing Selenium to connect to it.
- Overrides the
- selenium:
- Uses the
selenium/standalone-chromeimage to provide a browser environment for integration tests. - The
SE_VNC_NO_PASSWORDand 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, and7900are exposed for interacting with the Selenium server and VNC.
- Uses the
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 thewebcontainer.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 thewebcontainer.psql: Connects to the PostgreSQL database using the credentials from the.envfile.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:
Dockerfiledocker-compose.override.ymldocker-compose.test.ymldocker-compose.ymldocker-entrypoint-web.shrun
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 bashto enter the web container- Depending on how you’ve cloned the project, you may need to executed
chmod +x docker-entrypoint-web.shto make the file executable
- Depending on how you’ve cloned the project, you may need to executed
Now, inside the container run:
gem install rails -v 7.2rails 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, port3001is used, while Rails dev server is running on port3000by 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_seleniumthat 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_HOSTand theSELENIUM_REMOTE_PORTenvironment variables are set by thedocker-compose.test.ymlfile.
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_seleniumandconfig.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?
- Start your own project or play with the one from this article
- https://github.com/viniciusoyama/rails-7.2-docker-compose-setup-example
- Nothing solidifies learning more than doing it!
- Try new gems, add services and break things on purpose to see how they work. Docker makes it easy to reset and start fresh.
Thanks for reading to the and!
Subscribe to my newsletter!
I share content about Software Development & Architecture, Entrepreneurship and Lifelong Learning




