Today, I was browsing Hacker News when I stumbled upon an interesting project: coderunner-ui. The premise was compelling – a local-first AI workspace that lets you chat with LLMs and execute generated code in isolated environments, all without sending your data to the cloud. As someone who’s always looking for tools that respect privacy while providing powerful capabilities, this caught my attention immediately.
I cloned the repository, excited to try it out. Then I hit a wall: “Requires macOS on Apple Silicon.”
I use an Intel Mac. The Apple container system that coderunner-ui depends on is only available on Apple Silicon Macs.I have spent considerable time last few weeks solving something similar so I decided to dig deeper.
After examining the codebase, I understood why the Apple Silicon requirement existed. The project used Apple’s container system extensively – not just as a Docker alternative, but deeply integrated into its architecture. The install.sh script was full of commands like container system dns create local and container system start. The code execution happened inside an Apple Container VM using instavm/coderunner.
This wasn’t a simple case of replacing one container runtime with another. The entire system was built around Apple-specific technology.
I decided to use Claude Code to help with the migration. My first prompt was simple: “Analyze the current project code and README to understand the project. We want to replace Apple containers with Docker as I am not on Apple silicon.”
What followed was an interesting collaborative process. Claude Code and I worked through the codebase together, identifying dependencies and understanding the architecture. The install script rellied on the instavm/coderunner a secure local sandbox to run LLM-generated code using Apple containers
Claude suggested to use Datalayer Jupyter MCP Server to replace coderunner.
When we ran the application we were getting connection errors. The MCP server wasn’t responding.
My next prompt to Claude Code: “We are getting error when we run call the chat API. It looks like issue with MCP server read the documentation pages to run the MCP server correctly.”
This led us down a rabbit hole of documentation. We discovered that the Datalayer Jupyter MCP Server (the recommended replacement) had a completely different architecture than what we initially assumed. Instead of running everything in containers, the documentation recommended running JupyterLab in a Python virtual environment and only containerizing the MCP server.
While setting up the virtual environment, I noticed something odd. The installation was failing with package version conflicts. When I checked, I realized the virtual environment was using Python 3.8, even though I had Python 3.12.3 installed.
“We have Python Python 3.12.3 installed so why we are using 3.8?” I asked Claude Code.
It turned out the script was blindly using python3, which on my system pointed to an older version. Such a simple issue, but it would have caused hours of debugging without catching it early.
The migration became a lesson in architectural flexibility. Instead of forcing everything into Docker, we adopted a hybrid approach:
- JupyterLab runs in a Python virtual environment at
~/.coderunner/jupyter-venv - The MCP server runs in a Docker container
- They communicate over localhost (or
host.docker.internalon macOS/Windows)
When I asked Claude Code to “do that in virtualenv”, it restructured the entire installation process. The script now:
- Detects the best available Python version
- Creates a virtual environment with Python 3.12
- Installs the exact package versions specified in the documentation
- Starts JupyterLab in the background
- Runs the MCP server in Docker with proper network configuration
One interesting challenge was handling the difference between Linux and macOS/Windows. Docker on macOS and Windows can’t access host services via localhost from inside containers. They need to use host.docker.internal instead.
The solution was elegant – detect the platform and set the appropriate URL:
if [[ "$OSTYPE" == "darwin"* ]] || [[ "$OSTYPE" == "msys" ]]; then
host_url="http://host.docker.internal:8888"
else
host_url="http://localhost:8888"
fi
This migration was completed using Claude Code. The entire conversation, from initial analysis to final testing, took about two hours. The resulting codebase is simpler, more portable, and maintains all the original functionality.
I have pushed changes to GitHub. This commit has all the changes https://github.com/shekhargulati/coderunner-docker/commit/2e6f50fd15074d9ea418cdba86f52489ae3eaf54
Discover more from Shekhar Gulati
Subscribe to get the latest posts sent to your email.