Debugging your Phoenix, Elixir and Rails applications inside Docker containers

Khaja Minhajuddin
Tripping Engineering
3 min readAug 28, 2018

--

Elixir has a great REPL(iex) which allows you to run arbitrary code and see its results. And the same `iex` can be used to start your mix app with an attached REPL. If you are using phoenix you can get this up by running `iex -S mix phx.server` instead of `mix phx.server`.

[info] GET /
[debug] Processing with MMWeb.DevController.index/2
Parameters: %{}
Pipelines: [:browser]
Request to pry #PID<0.618.0> at MMWeb.DevController.index/2 (lib/mm_web/controllers/dev_controller.ex:7)
5:
6: def index(conn, _params) do
7: require IEx; IEx.pry
8: render(conn, [])
9: end
Allow? [Yn]

You can just hit enter or `Y` and it will drop you into the REPL with the current process’s context, You can now inspect `conn` or `params` or any variable in the current context. This allows you to inspect things in a much more interactive fashion. Once you are done with your debugging you can just enter the `continue` command to let the process continue after the breakpoint.

Rails has a similar experience using the `pry` gem. You can just add `binding.pry` to a code path where you want to stop and inspect things and it too will stop when it hits the `binding.pry` statement.

This is all fine and dandy when you are running your applications on your local computer. However, most applications these days run inside docker containers even on developers’ machines, either as a one off docker container or as part of a docker compose script which spins up other dependent applications as individual docker containers. And things become very tricky when you want to use a debugger in these situations. One easy way to latch on to the REPL if you are using Elixir/Erlang is to connect to the BEAM node inside the container from your local computer as a remote shell. And for rails applications, I’ve seen folks start a new rails server on a different port from inside the container just to use the debugger using the docker exec command:

# from the local computer
$ docker exec -it rails_app_container_name /bin/bash
# now we are inside the container in a bash shell
$ rails server -p 8080

And once they have this, they target this port and latch on to the debugger through`docker exec`.

A better way

Thankfully docker provides a better way to do this. When you run a docker container in detached mode (which is what development scripts usually do), you can attach to them by running the `docker attach` command.

# attach to a running docker container whose name is 
# rails_app_container_name
docker attach rails_app_container_name

And once you attach to a container it will drop you into the context of your container’s `CMD`. So, if you have a rails server, it will show you the rails logs. However, just doing this doesn’t give you the ability to use `binding.pry` or the debugger. To be able to attach an interactive terminal to a detached container you’ll need to start the containers using `docker run -it` which keeps the `stdin` open (-i, — interactive) and allocates a pseudo-TTY (-t, — tty) for this container. And, if you are using `docker-compose` you’ll have to set it up using the `stdin_open` and the `tty` configuration options.

version: "3"services:
myapp:
image: myapp
stdin_open: true
tty: true
# ...

Once you’ve set these up you can attach to your container using `docker attach container_name` and start using your REPLs without any problems. And when you are done with your REPL session you can detach from it by hitting the `Ctrl+P Ctrl+Q` combo.

Demo

Let us try all of this in a small demo:

# open a new irb session in a detached container and store its
# container id
container_id=$(docker run --detach --interactive --tty ruby:latest irb)
# attach to the previous container
docker attach "$container_id"
# run a few ruby commands from irb
# irb(main):002:0> puts "ruby is great".upcase
# ...
# hit Ctrl+P and Ctrl+Q to detach from this container to get back to your local shell
# irb(main):003:0> read escape sequence

That is all you need to get debuggers working in docker containers.

--

--