Published 2022-02-21.
Last modified 2022-04-17.
Time to read: 9 minutes.
This blog post is the last in a three-part series.
The first post
describes how to install and use the jekyll_bootstrap5_tabs
plugin.
The second post describes how the Jekyll plugin was constructed.
The plugin is built and published as an open-source Ruby gem.
This blog post is dedicated to demonstrating a straightforward way of debugging Jekyll plugins,
which are always written in Ruby.
Many open-source projects fall far short of their true potential because no-one bothers to tell the story, completely and thoughtfully, in depth. Hopefully this post will in some small way improve Jekyll’s circumstance in the F/OSS world.
The Jekyll documentation provides absolutely no commentary on how to debug plugins. Perhaps the Jekyll developers felt the information would be obvious to sufficiently experienced programmers. Because so many debugging possibilities exist, I did not find it obvious.
These Instructions Work Everywhere
The instructions in this post should work on every OS that Jekyll runs on, using Visual Studio Code, RubyMine, IntelliJ IDEA, or any other IDE that supports normal Ruby debugging.
Most problems with debugging Jekyll plugins are related to the plugin’s need to receive parameters from Jekyll. We do not need to dig into Jekyll itself to get this sorted out; all we need to do is to find where the gem you want to debug was installed, and set breakpoints. The information that the IDE will then present to you will far exceed what the Jekyll plugin documentation provides.
Prerequisites
The Setting Up a Ruby Development Environment post describes the necessary preparations.
Plugins Run In the Jekyll Address Space
The most important thing to know about debugging Jekyll plugins is that they run in the same address space as Jekyll itself. That means to debug a plugin you must actually debug the Jekyll process, and it will load the most recently created version of your plugin.
Orientation
For the purposes of this blog post, I assume that you have a Jekyll plugin that you want to debug.
It does not matter if you wrote the plugin or not.
All that matters is that the plugin was installed properly.
Throughout this blog post, I will refer to this gem as “the subject gem”.
I wrote this post to figure out how to debug my subject gem, which is
jekyll_bootstrap5_tabs
,
so you will see references to that gem in this post whenever I discuss what needs to be done with your subject gem.
Move to your Jekyll project directory, this is where we will work. For me, that meant:
$ cd $jekyll_bootstrap5_tabs
If you are curious where the environment variable above was defined, when my Bash shells start, they source $work/.evars
.
The following lines are within:
export work=/var/work export jekyll=$work/jekyll export jekyll_flexible_include_plugin=$jekyll/jekyll-flexible-include-plugin export jekyll_bootstrap5_tabs=$jekyll/jekyll_bootstrap5_tabs export jekyll_template=$sites/jekyll_template
That is part of a directory structure I maintain across several machines.
Jekyll Launcher
Jekyll is provided as a Ruby gem.
When the Jekyll gem is installed, it also creates a launcher at /usr/local/bin/jekyll
,
which is actually a small Ruby program that loads the gem.
#!/usr/bin/env ruby2.7 # # This file was generated by RubyGems. # # The application 'jekyll' is installed as part of a gem, and # this file is here to facilitate running it. # require 'rubygems' version = ">= 0.a" str = ARGV.first if str str = str.b[/\A_(.*)_\z/, 1] if str and Gem::Version.correct?(str) version = str ARGV.shift end end if Gem.respond_to?(:activate_bin_path) load Gem.activate_bin_path('jekyll', 'jekyll', version) else gem "jekyll", version load Gem.bin_path("jekyll", "jekyll", version) end
Jekyll is best debugged when launched via the normal means, that is, via /usr/local/bin/jekyll
.
The launcher figures out where the Jekyll gem resides, and loads it.
If you set a breakpoint on the installed plugin's source code, execution will halt when the plugin is loaded or invoked. You will be able to see the call stack and the variables at each level of the call stack. This is super helpful!
Locating the Subject Gem
You need to know where the subject gem was installed to set breakpoints in it. From within a Jekyll project that uses the subject gem, discover the location of the Jekyll entry points within the gem as follows:
$ bundle info jekyll_bootstrap5_tabs * jekyll_bootstrap5_tabs (1.1.0) Summary: Jekyll plugin for Bootstrap 5 tabs Homepage: https://mslinn.com/blog/2022/02/13/jekyll-gem.html Source Code: https://github.com/mslinn/jekyll_bootstrap5_tabs Path: /home/mslinn/gems/gems/jekyll_bootstrap5_tabs-1.1.0
The jekyll_bootstrap5_tabs
gem is located at /home/mslinn/gems/gems/jekyll_bootstrap5_tabs-1.1.0
.
Now we need to find the entry point for the plugin,
which is where Jekyll invokes the functionality of the plugin.
If we search for Liquid::
we'll find the source files containing the entry points.
$ $ grep -rl 'Liquid::' /home/mslinn/gems/gems/jekyll_bootstrap5_tabs-1.1.0/* /home/mslinn/gems/gems/jekyll_bootstrap5_tabs-1.1.0/lib/jekyll_bootstrap5_tabs.rb
Now we know we need to open /home/mslinn/gems/gems/jekyll_bootstrap5_tabs-1.1.0/lib/jekyll_bootstrap5_tabs.rb
in Visual Studio to set breakpoints.
Here is a one-line command that will tell you that same information:
$ grep -rl 'Liquid::' "$( bundle info jekyll_bootstrap5_tabs | \ grep Path | awk '{print $2}' )" /home/mslinn/gems/gems/jekyll_bootstrap5_tabs-1.1.0/lib/jekyll_bootstrap5_tabs.rb
Launching vs. Attaching
For programs that you built, you would start the debug server; it would launch the program and provide normal Ruby debugging features. Your IDE (Visual Studio Code, RubyMine or IDEA) attaches to the debug server. Most IDEs simplify this process by allowing you to provide a run configuration for launching a Ruby program / gem; the IDE takes care of the plumbing.
Because the Jekyll launch process is complex, we will start it up and then attach to its process instead.
Debugging using Visual Studio Code and Docker on Debian Distros
Documentation on how to set up Jekyll for debugging using Visual Studio Code has been available for Debian-derived Linux distros since February 2020. However, the docs are thin and are not applicable to many circumstances. Following the instructions causes Docker and other dependencies to be installed on your machine.
I don't see any benefit in running Docker to debug Jekyll.
Examining the Dockerfile
reveals its essence, however:
two well-known gems are installed that provide a debug server
ruby-debug-ide
and
debase
(GitHub).
.devcontainer Files
You can skip ahead to Container-Free Debugging. I just provide the information in this section for completeness, but it is not necessary for understanding anything relevant to this blog post.
Here are the files included in Jekyll for debugging via Docker, in the .devcontainer
directory:
devcontainer.json
and Dockerfile
.
// For format details, see https://aka.ms/vscode-remote/devcontainer.json or this file's README at: // https://github.com/microsoft/vscode-dev-containers/tree/v0.101.1/containers/ruby-2 { "name": "Ruby 2", "dockerFile": "Dockerfile", // Set *default* container specific settings.json values on container create. "settings": { "terminal.integrated.shell.linux": "/bin/bash" }, // Add the IDs of extensions you want installed when the container is created. "extensions": [ "rebornix.Ruby" ] // Use 'forwardPorts' to make a list of ports inside the container available locally. // "forwardPorts": [], // Use 'postCreateCommand' to run commands after the container is created. "postCreateCommand": "bundle install", // Uncomment to connect as a non-root user. See https://aka.ms/vscode-remote/containers/non-root. // "remoteUser": "vscode" }
The following Dockerfile
is specific to Debian Linux and related distros that support apt
.
Sorry, Mac users!
#------------------------------------------------------------------------------------------------------------- # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. See https://go.microsoft.com/fwlink/?linkid=2090316 for license information. #------------------------------------------------------------------------------------------------------------- FROM ruby:2 # Avoid warnings by switching to noninteractive ENV DEBIAN_FRONTEND=noninteractive # This Dockerfile adds a non-root user with sudo access. Use the "remoteUser" # property in devcontainer.json to use it. On Linux, the container user's GID/UIDs # will be updated to match your local UID/GID (when using the dockerFile property). # See https://aka.ms/vscode-remote/containers/non-root-user for details. ARG USERNAME=vscode ARG USER_UID=1000 ARG USER_GID=$USER_UID # Configure apt and install packages RUN apt-get update \ && apt-get -y install --no-install-recommends apt-utils dialog locales 2>&1 \ # Verify git, process tools installed && apt-get -y install git openssh-client iproute2 procps lsb-release \ # # Install ruby-debug-ide and debase && gem install ruby-debug-ide \ && gem install debase \ # # Install node.js && apt-get -y install curl software-properties-common \ && curl -sL https://deb.nodesource.com/setup_13.x | bash - \ && apt-get -y install nodejs \ # # Create a non-root user to use if preferred - see https://aka.ms/vscode-remote/containers/non-root-user. && groupadd --gid $USER_GID $USERNAME \ && useradd -s /bin/bash --uid $USER_UID --gid $USER_GID -m $USERNAME \ # [Optional] Add sudo support for the non-root user && apt-get install -y sudo \ && echo $USERNAME ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/$USERNAME\ && chmod 0440 /etc/sudoers.d/$USERNAME \ # # Clean up && apt-get autoremove -y \ && apt-get clean -y \ && rm -rf /var/lib/apt/lists/* # Set the locale RUN sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen && \ dpkg-reconfigure --frontend=noninteractive locales && \ update-locale LANG=en_US.UTF-8 ENV LANG en_US.UTF-8 # Switch back to dialog for any ad-hoc use of apt-get ENV DEBIAN_FRONTEND=dialog
Container-Free Debugging
These instructions are applicable for all platforms that Jekyll runs on.
Debugging Components
A diagram should help us understand how the different components of a Ruby debugging extension work for Visual Studio Code. I added Ruby components to a diagram provided in Introducing Logpoints and auto-attach from the Visual Studio Code documentation.

You can see three components in the above diagram:
- The
rebornix.Ruby
Visual Studio Code plugin, which provides debugging, lint, Semantic highlighting, and Intellisense support for Ruby. - The
ruby-debug-ide
gem, which communicates between Visual Studio and Ruby debuggers. - The
debase
gem, which is the actual Ruby debugger.
Install Debugging Gems
Install the two Ruby gems required for debugging.
The debase-ruby_core_source
transitive dependency was automatically installed as well.
$ gem install debase ruby-debug-ide Fetching debase-0.2.4.1.gem Fetching debase-ruby_core_source-0.10.14.gem Successfully installed debase-ruby_core_source-0.10.14 Building native extensions. This could take a while... Successfully installed debase-0.2.4.1 Parsing documentation for debase-ruby_core_source-0.10.14 Installing ri documentation for debase-ruby_core_source-0.10.14 Parsing documentation for debase-0.2.4.1 Installing ri documentation for debase-0.2.4.1 Done installing documentation for debase-ruby_core_source, debase after 4 seconds Fetching ruby-debug-ide-0.7.3.gem Building native extensions. This could take a while... Successfully installed ruby-debug-ide-0.7.3 Parsing documentation for ruby-debug-ide-0.7.3 Installing ri documentation for ruby-debug-ide-0.7.3 Done installing documentation for ruby-debug-ide after 0 seconds 3 gems installed
Lets find out the names of the commands provided by the ruby-debug-ide
gem:
$ gem specification ruby-debug-ide executables --- - rdebug-ide - gdb_wrapper
rdebug-ide Command-Line Options
Now lets discover the command-line options for the rdebug-ide
command provided by the ruby-debug-ide
gem:
$ bundle exec rdebug-ide Using ruby-debug-base 0.2.4.1 Usage: rdebug-ide is supposed to be called from RDT, NetBeans, RubyMine, or the IntelliJ IDEA Ruby plugin. The command line interface to ruby-debug is rdebug. Options: -h, --host HOST Host name used for remote debugging -p, --port PORT Port used for remote debugging --dispatcher-port PORT Port used for multi-process debugging dispatcher --evaluation-timeout TIMEOUT evaluation timeout in seconds (default: 10) --evaluation-control trace to_s evaluation -m, --memory-limit LIMIT evaluation memory limit in mb (default: 10) -t, --time-limit LIMIT evaluation time limit in milliseconds (default: 100) --stop stop when the script is loaded -x, --trace turn on line tracing --skip_wait_for_start skip wait for 'start' command -l, --load-mode load mode (experimental) -d, --debug Debug self - prints information for debugging ruby-debug itself --xml-debug Debug self - sends informations for debugging ruby-debug itself -I, --include PATH Add PATH to $LOAD_PATH --attach-mode Tells that rdebug-ide is working in attach mode --key-value Key/Value presentation of hash items --ignore-port Generate another port --keep-frame-binding Keep frame bindings --disable-int-handler Disables interrupt signal handler --rubymine-protocol-extensions Enable all RubyMine-specific incompatible protocol extensions --catchpoint-deleted-event Enable chatchpointDeleted event --value-as-nested-element Allow to pass variable’s value as nested element instead of attribute --socket-path PATH Listen for debugger on the given UNIX domain socket path Common options: -v, --version Show version Must specify a script to run
The gdb_wrapper
executable provided by ruby-debug-ide
gem is meant to be invoked by the IDE:
$ bundle exec gdb_wrapper You should specify PID of process you want to attach to
The debase
gem does not provide any executables:
$ gem specification debase executables --- []
Install Visual Studio Code Ruby Support
Install the rebornix.Ruby
Visual Studio Code plugin by Peng Lv.
This plugin provides Ruby language support and debugging for Visual Studio Code.
When I last checked, it had 2,395,349 installations!
Learn about Visual Studio Code debugging for Ruby.
You can do the installation via the command line if you like:
$ code --install-extension rebornix.Ruby@0.28.1
The debase
gem
is a fast implementation of the standard Ruby debugger debug.rb for Ruby 2.0.
The core component provides support that front-ends can build on.
It provides breakpoint handling, bindings for stack frames, and more.
Debase
relies on debase-ruby_core_source
to work with Visual Studio Code,
however by default debase
pulls in a very old version of
debase-ruby_core_source
, released in 2017.
That old version does not support conditional breakpoints, for example.
Add the following to your project Gemfile
to get current versions of both gems:
gem 'debase', require: false gem 'debase-ruby_core_source', '>= 0.10.15', require: false
Now install the new gems declared in Gemfile
:
$ bundle install
Caveat
Looking at the source code for the Ruby plugin it is apparent that functional breakpoints are not implemented. This is not mentioned in the documentation.
Setup Debugging
Now it is time to launch Jekyll from the rdebug-ide
debug server.
Debug clients such as Visual Studio Code will be able to control the debug session via port 1234.
At first, I specified all of my normal Jekyll options for local usage, as shown below. They probably impacted the responsiveness of the debug session, but are workable nonetheless.
$ bundle exec rdebug-ide \ --host 0.0.0.0 \ --port 1234 \ --dispatcher-port 26162 \ -- \ /usr/local/bin/jekyll serve \ --livereload_port 35721 \ --force_polling \ --host 0.0.0.0 \ --port 4001 \ --future \ --incremental \ --livereload \ --drafts \ --unpublished Fast Debugger (ruby-debug-ide 0.7.3, debase 0.2.4.1, file filtering is supported) listens on 0.0.0.0:1234
Jekyll does not run any slower under the Ruby debugger.
Above you see 2-stage launches expressed as single command lines. The backslashes are line continuation characters, which is why 'one command line' actually spans a dozen or so lines.
- Stage 1: debugger parameters
- Stage 2: Jekyll parameters
I took a screen shot of the above command line and added a few comments:

You could make the above command line into a script, or a Visual Studio Code task. Here it is as a task:
{
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
"version": "2.0.0",
"tasks": [
{
"label": "start-debug",
"command": "/usr/local/bin/bundle",
"args": [
"exec", "rdebug-ide",
"--host", "0.0.0.0",
"--port", "1234",
"--dispatcher-port", "26162",
"--",
"/usr/local/bin/jekyll", "serve",
"--livereload_port", "35721",
"--force_polling",
"--host", "0.0.0.0",
"--incremental",
"--livereload",
"--port", "4001",
"--future",
"--drafts",
"--unpublished",
],
"isBackground": true,
"presentation": {
"panel": "new"
},
"problemMatcher": {
"owner": "custom",
"pattern": {
"regexp": "____"
},
"background": {
"activeOnStart": true,
"beginsPattern": "____",
"endsPattern": "Fast Debugger"
}
}
}
]
}
I made the following run configuration for Visual Studio Code to attach to the process resulting from running the above.
Note that this is not the entire contents of .vscode/launch.json
, just the one run configuration.
{ "cwd": "${workspaceRoot}", "name": "Attach rdebug-ide", "request": "attach", "remoteHost": "localhost", "remotePort": "1234", "remoteWorkspaceRoot": "/", "restart": true, "showDebuggerOutput": true, "stopOnEntry": true, "type": "Ruby", },
I did not have success when I attempted to run the task automatically when starting the debug session.
I am unsure what the problem was.
If you are so inclined, you could enable that feature by adding the following to the
launch.json
entry above. I thoughtfully provided a trailing comma for you to copy:
"preLaunchTask": "start-debug",
Step By Step
To establish a debugging session:
-
Launch Jekyll as shown above (
bundle exec rdebug-ide …
), either on the command line or as a Visual Studio Code task. - Set a breakpoint in Visual Studio Code (I will discuss this next).
- Attach the Visual Studio debug client using the run configuration called
Attach rdebug-ide
.
Breakpoint Types
Visual Studio Code supports several types of breakpoints. This post discusses only two types: location-based breakpoints and function breakpoints. The links in this paragraph explain how breakpoints work in detail; I won't repeat that information here.
However, the rebornix.Ruby
Visual Studio plugin does not support function breakpoints.
The overhead of breakpoints can be reduced.
PDB Files
Visual Studio Code uses PDB files to associate source code line numbers and variable names with a program being debugged. However, I am unaware of any mechanism to create a PDB file for Ruby code. Dear reader, if you know how to do this, please contact me.
This means that attaching to a Ruby process, or debugging a gem that was launched, will not automatically cause the relevant source file to open up at the line containing the breakpoint when hit. Instead, you will have to look at the top item in the Call stack pane, where you will see the fully qualified name of the source file and the line number where execution has been paused. You will then have to open that file up in the editor manually.

Execution Break - Ta-Da!
When VS Code hits a breakpoint, the call stack and variables are shown, but the editor does not display the line that the debugger has stopped at. You might not realize that this happened. After all this work (I only publish the happy path), when this just happened as it should for me the first time, I did not recognize it. If a source edit pane opened up with the breakpoint highlighted I would have noticed. Unfortunately, that does not work (yet!).

Now for the ta-da! moment. If you are not a programmer, this will go over with a thud. Since you made it this far, you are either a masochist, or a programmer.
Take a good long look at the variables in the following screenshot. You can see what has been passed from Jekyll to the plugin easily here. This information is really hard to come by any other way.
Context.@environments

context.instance_variable_get('@scopes')
, an array of hashes, has only one entry: 'nowMillis': '1645386226'
.
That was a Liquid variable that I had set in the page that contained a reference to the Bootstrap 5 tabs plugin.
self
The debugger paused at the start of TabsBlock#render
.
The variable self
has type JeklyyBootstrap5Tabs::TabsBlock
,
and it looks like this:

The Jekyll tag was: {% tabs test pretty %}
:
self.@markup
is a string with valuetest pretty
(with a space at the end).self.@pretty_print
istrue
.self.@tab_name
is a string with valuetest
.self.tag_name
is a string with valuetabs
.
About the Author
trueI, Mike Slinn, have been working with Ruby a long time now. Back in 2005 I was the product marketing manager at CodeGear (the company was formerly known as Borland) for their 3rd Rail IDE. 3rd Rail supported Ruby, and Ruby on Rails, at launch.
In 2006, I co-chaired the Silicon Valley Ruby Conference, on behalf of SD Forum in Silicon Valley. As you can see, I have the t-shirt. I was sole chairman of the 2007 Silicon Valley Ruby Conference.
Several court cases have come my way over the years in my capacity as a software expert witness. The court cases featured questions about IP misappropriation for Ruby on Rails programs. You can read about my experience as a software expert if that interests you.
I currently enjoy writing Jekyll plugins in Ruby for this website and others.