Remote Debugging a Java Process on Heroku
Java processes on Heroku run inside of a dyno, which has a few restrictions that make it difficult to attach debuggers and management consoles. But it’s not impossible.
My Java Debug Wire Protocol (JDWP) Buildpack can be added to your Heroku application with just a few simple commands. It will use ngrok to proxy the debug session on Heroku, making it externally accessible. Or you can run ngrok locally to have your Java process connect to you. I’ll begin with the former.
Connect from your local debugger
First, create a free ngrok account. This is necessary to use TCP with their service. Then capture your API key, and set it as a config var on your Heroku app like this:
$ heroku config:set NGROK_API_TOKEN=xxxxxx
Next, add the JDWP buildpack to your app:
$ heroku buildpacks:add https://github.com/jkutner/heroku-buildpack-jdwp.git
Then add your primary buildpack. For example, if you are using Java:
$ heroku buildpacks:add https://github.com/heroku/heroku-buildpack-java.git
Now modify your Procfile
by prefixing your web
process with the with_jdwp
command. For example:
web: with_jdwp java $JAVA_OPTS -cp target/classes:target/dependency/* Main
Finally, commit your changes, and redeploy the app:
$ git add Procfile
$ git commit -m "Added with_jdwp"
$ git push heroku master
Once your app is running with the JDWP buildpack and the with_jdwp
command, you’ll see something like this in your logs:
2015-05-19T16:06:36.530988+00:00 app[web.1]: Listening for transport dt_socket at address: 8998
...
2015-05-19T16:06:37.052977+00:00 app[web.1]: [05/19/15 16:06:37] [INFO] [client] Tunnel established at tcp://ngrok.com:39678
Then, from your local machine, you can connect to the process using the ngrok URL from the logs. For example:
$ jdb -attach ngrok.com:39678
Set uncaught java.lang.Throwable
Set deferred uncaught java.lang.Throwable
Initializing jdb ...
>
Now you can use it:
> methods my.company.MainServlet
...
javax.servlet.Servlet service(javax.servlet.ServletRequest, javax.servlet.ServletResponse)
javax.servlet.Servlet getServletInfo()
javax.servlet.Servlet destroy()
javax.servlet.ServletConfig getServletName()
javax.servlet.ServletConfig getServletContext()
javax.servlet.ServletConfig getInitParameter(java.lang.String)
javax.servlet.ServletConfig getInitParameterNames()
Or just use your favorite IDE. Your favorite is IntelliJ IDEA right?
Connect to your local debugger
If you’d like to have your process connect to your local machine (going the opposite direction) you can install ngrok locally. Then run it
./ngrok -proto=tcp 9999
This will display the ngrok URL. Use it to set the following config vars on your Heroku app:
$ heroku config:set JDWP_PORT="ngrok.com:39678"
$ heroku config:set JDWP_OPTS="server=n,suspend=y"
Finally, start your debugger locally:
jdp -listen 9999
And redeploy your app with git push
.