Debugging Ruby on Rails running in a Docker Container

Published: Wednesday, October 12, 2022

Greetings, friends! In Part 2 of this series, we learned how to setup a new Rails application and how to debug it using the rdbg executable that comes installed with the debug gem. In this article, I will discuss how to use rdbg as a remote debugger that can connect to a Rails app running inside a Docker container!

Installing rdbg 1.5

For this tutorial, we will actually have to downgrade the version of the debug gem because, as mentioned in this GitHub issue, the latest version of rdbg, version 1.6.x, fails to work properly when debugging remotely. When this issue is resolved, I'll update this article in the future. However, if you're trying to debug a Rails application running in a Docker container, you'll need to make sure both your local machine and the Docker image installs version 1.5 of the debug gem.

Navigate to the Gemfile located inside the blog directory of the sample Rails app we created in Part 2 and look for the following line:

ruby
Copied! ⭐️
gem "debug", platforms: %i[ mri mingw x64_mingw ]

Replace this line with the following:

ruby
Copied! ⭐️
 gem "debug", "1.5"

Then, run the following command inside the blog directory:

text
Copied! ⭐️
rm Gemfile.lock && bundle

This will remove the Gemfile.lock file and run a bundle install using bundler to download version 1.5 of the debug gem.

Adding the Dockerfile

For this tutorial, I will assume you have Docker installed on your computer. I am using Docker Desktop for Mac to setup my Docker environment, so I can build Docker files, run containers, and run Docker Compose.

We will be creating four new files inside the blog directory. By the end of this entire tutorial, your directory structure should look similar to the following:

List of all files that should be in the blog directory after completing this tutorial.

First, let's create a file named Dockerfile inside the blog directory. Add the following contents to it:

text
Copied! ⭐️
FROM ruby:3.0.0
WORKDIR /app
COPY Gemfile* .
RUN bundle install
COPY . .
RUN chmod +x docker-entrypoint.sh

The Dockerfile will be used to create a Docker image that we'll run our sample Rails application in. Let's take a closer look at what each command does.

FROM ruby:3.0.0 - We start from the Official Docker Image for Ruby, using version 3.0.0.

WORKDIR /app - Set the current directory to /app when the container starts.

COPY Gemfile* . - Copy the Gemfile and Gemfile.lock files from our blog directory to the container.

RUN bundle install - Use Bundler to install all the gems specified in our Gemfile.

COPY . . - Copy everything from the blog directory to the Docker container.

RUN chmod +x docker-entrypoint.sh - Make the docker-entrypoint.sh file executable. This script is used to cleanup any stray server.pid files that may cause problems when starting the Puma server in the Docker container.

Ignoring Files

Let's add a .dockerignore file inside the blog directory with the following contents:

text
Copied! ⭐️
Dockerfile

You can add whatever you want to this file, and they'll all be ignored by Docker when building the Docker image.

Creating a Cleanup Script

Next, let's create the docker-entrypoint.sh file inside the blog directory and insert the following:

shell
Copied! ⭐️
#!/bin/bash
set -e

if [ -f tmp/pids/server.pid ]; then
  rm tmp/pids/server.pid
fi

exec bundle exec "$@"

You may be wondering why this file is necessary. When I was running the Docker container to start the Puma server used by Ruby on Rails, the server sometimes fails to stop even after the Docker container is closed down.

There is a good discussion on StackOverflow that talks about this issue. It's also where I found the docker-entrypoint.sh script. This script will remove any stray server.pid files. The server.pid file is used to store the process ID (pid) of a process, so the server can stop itself.

Setting up the Docker Compose Config

Next, let's create a new file called docker-compose.yaml with the following contents:

yaml
Copied! ⭐️
version: '3.9'
services:
  app:
    image: rails-app
    entrypoint: /app/docker-entrypoint.sh
    command: [rdbg, -n, --open, --host, 0.0.0.0, --port, '12345', -c, --, rails, server, -b, 0.0.0.0]
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWD: example
      POSTGRES_HOST: db
    ports:
      - 3000:3000
      - 12345:12345
  db:
    image: postgres:alpine3.15
    environment:
      POSTGRES_PASSWORD: example

We are using Docker Compose to simplify the process of exposing ports that will provide bidirectional communication between our local machine and the Docker container. We are also using it to create a Postgres image that will run the Postgres database needed by our sample Ruby on Rails project.

Docker Compose will run the image called rails-app and use /app/docker-entrypoint.sh as the entrypoint which means that this script will run as soon as the Docker container runs. Then, it'll try running the following command:

text
Copied! ⭐️
rdbg -n --open --host 0.0.0.0 --port 12345 -c -- rails server -b 0.0.0.0

This command will use rdbg to run the Puma server as a debuggee. We will later attach to it with a debugger using the rdbg -A command.

The -n option will make sure that the the debugger doesn't immediately start at the beginning of a script (which likely would have been at the start of the /bin/rails.rb file).

The --open option will start remote debugging and open a network port. By default, it'll try to open a Unix domain socket (UDS). Since we specify the --port option, it'll use a TCP connection instead. In this case, we're running rdbg on port 12345 and using a wildcard mask for the host, 0.0.0.0.

The -c option tells rdbg to execute a command or script. In this case, we're telling it to execute rails server -b 0.0.0.0 which will start the Puma server inside the Docker container using a host of 0.0.0.0.

Running the Docker Container

Finally, we have everything needed to build and run the Docker container! We can build a Docker image using the following command in the blog directory, the directory that contains our Dockerfile:

text
Copied! ⭐️
docker build -t rails-app .

This will create a new Docker image called rails-app, the same name we specified in the docker-compose.yaml file.

Next, we can run the following command to use Docker Compose to orchestrate (or run) both our newly created Ruby on Rails container and a Postgres container:

text
Copied! ⭐️
docker-compose up

If everything is successful, you should have a Ruby on Rails application running in a Docker container! If you go to http://localhost:3000, then you should be able to see the sample page provided by Ruby on Rails.

Let's test that the debugger is working. Navigate to http://localhost:3000/articles/index. Inside your terminal, you should see something similar to the following:

text
Copied! ⭐️
DEBUGGER: wait for debugger connection...

Open a new terminal tab or window and navigate to the blog directory. Run the following command:

text
Copied! ⭐️
bundle exec rdbg -A 12345

This will instruct bundler to use version 1.5 of rdbg and attach to a debuggee process running on port 12345. In our case, the Ruby on Rails application is running on port 12345 and since Docker Compose exposed this port, we can send communication between port 12345 of our local machine and port 12345 of the Docker container.

You now have access to a remote debugger and can debug Ruby on Rails application running in a Docker container from your local machine!

In a separate tab, you can use docker-compose down to terminate both the rails-app and postgres:alpine3.15 containers. This is better than using something like Ctrl + C to terminate your containers because it allows Docker Compose to clean up the processes.

You can then use docker-compose up to restart them again.

Conclusion

In this article, we have learned how to remotely debug a Ruby on Rails application running inside a Docker container! Impress your friends, troubleshoot issues, and have fun!