Debugging Ruby on Rails running in a Docker Container
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:
gem "debug", platforms: %i[ mri mingw x64_mingw ]
Replace this line with the following:
gem "debug", "1.5"
Then, run the following command inside the blog
directory:
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:
First, let's create a file named Dockerfile
inside the blog
directory. Add the following contents to it:
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:
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:
#!/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:
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:
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
:
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:
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:
DEBUGGER: wait for debugger connection...
Open a new terminal tab or window and navigate to the blog
directory. Run the following command:
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!