How I built a browser Java compiler with Monaco Editor
In the modern web era, developers expect instant feedback. Whether it's a sandbox, a tutorial, or an interactive technical interview, requiring a user to download a JDK and configure their IDE is a massive point of friction.
I wanted to build an experience similar to LeetCode or CodePen, but specifically for Java—a notoriously heavy and server-dependent language. Here is how I built a robust browser-based Java compiler using the Monaco Editor and isolated execution environments.
The Frontend: Monaco Editor
The first step was providing a world-class typing experience. You can't build a serious web IDE with a basic \.
I integrated Monaco Editor, the very same engine that powers VS Code. Setting up Monaco in a React environment is relatively straightforward thanks to wrappers like \@monaco-editor/react\, but configuring it for Java requires extra work.
Out of the box, Monaco provides basic syntax highlighting for Java, but it lacks semantic understanding (autocomplete, error linting, import suggestions). To achieve true IDE features, I had to connect Monaco to a Language Server Protocol (LSP). I set up a lightweight WebSocket connection between Monaco on the client and an Eclipse JDT Language Server running in a container. This allowed the editor to show real-time squiggly lines and intelligent code completion, dramatically elevating the user experience.
The Execution Challenge
Java isn't interpreted in the browser like JavaScript. It must be compiled (\javac\) to bytecode and then executed (\java\) by a JVM.
Initially, I considered compiling Java to WebAssembly (Wasm) to run entirely client-side. While projects like CheerpJ exist, they often come with significant overhead or limitations regarding the standard library and threading. I needed standard, predictable execution.
I opted for a highly isolated backend execution engine.
Building the Execution Pipeline
When a user clicks "Run", the following sequence happens:
1. Payload Transmission: The raw Java code string is sent via a REST API to a Node.js orchestration server.
2. Container Provisioning: The orchestrator talks to the Docker daemon to spin up an ephemeral, deeply restricted Docker container based on a minimal Alpine Linux OpenJDK image.
3. Compilation: Inside the container, the code is saved to a \Main.java\ file. A strict timeout (e.g., 5 seconds) is applied, and \javac\ is executed. If compilation fails, the stderr is piped directly back to the client and the container is killed.
4. Execution: If compilation succeeds, the \java\ command runs the compiled class. Again, a strict timeout and memory limit (e.g., 128MB) are enforced.
5. Output Streaming: The \stdout\ and \stderr\ are captured and streamed back to the frontend dashboard.
6. Cleanup: Regardless of success, failure, or timeout, the ephemeral container is immediately destroyed.
Security: The Ultimate Priority
Executing arbitrary, untrusted code from the internet is fundamentally dangerous. A malicious user could write Java code to attempt a fork bomb, mine cryptocurrency, or access internal network resources.
To mitigate this, the execution containers are locked down heavily:
- No Network Access: The containers are run without network interfaces (\
--network none\). - Read-Only Filesystem: The root filesystem is mounted as read-only.
- Resource Quotas: Strict CPU and memory limits are enforced at the Docker level.
- PIDs Limit: The maximum number of processes is capped to prevent fork bombs.
Closing Thoughts
Building a browser-based Java compiler was a fantastic exercise in balancing frontend user experience with hardcore backend security. The combination of Monaco Editor for the UI and ephemeral Docker containers for execution provides a seamless, safe, and powerful platform.
Mahesh Chopade builds developer tools and modern web experiences. Check out my other projects on my portfolio.