Welcome!

Slide to unlock and explore

Slide to unlock

Command Palette

Search for a command to run...

0
Blog
PreviousNext

Building Monaco - A VS Code Clone with Containerized Code Execution

Deep dive into building a secure, VS Code-like IDE with real-time code execution using Go, Docker, WebSockets, and Monaco Editor. Collaborative project with Ishika Bhoyar.

Project Overview

Monaco is a full-featured VS Code clone I built with Ishika Bhoyar that brings the power of containerized code execution to the browser. The project combines the Monaco editor (the same editor that powers VS Code) with a robust Go backend and Docker containerization to create a secure, interactive coding environment.

Live Demo: monaco.ishikabhoyar.tech
API Endpoint: api.ishikabhoyar.tech

Key Features:

  • VS Code-like interface with file tree navigation and tab management
  • Real-time code execution in isolated Docker containers
  • Interactive terminal with WebSocket-based I/O streaming
  • Support for 6 programming languages (Python, Java, C, C++, JavaScript, Go)
  • Secure execution with resource limits and network isolation
  • Persistent file structure using localStorage
  • Cloudflare Tunnel for secure external access

Tech Stack

The project uses a modern, containerized architecture:

Frontend:

  • React + Vite
  • Monaco Editor (Microsoft)
  • WebSocket client for real-time communication
  • Tailwind CSS for styling
  • localStorage for file persistence

Backend:

  • Go 1.19 with Gorilla Mux
  • Docker for containerized code execution
  • WebSocket server for real-time I/O
  • Job queue system for concurrent executions
  • CORS middleware for cross-origin requests

Infrastructure:

  • Docker + Docker Compose
  • Cloudflare Tunnel for secure public access
  • Nginx reverse proxy
  • Multi-stage Docker builds

Architecture Design

System Architecture

Frontend (React + Monaco)
   ↓ HTTP POST /api/submit
Express API Server
   ↓ Assign Submission ID
Job Queue (100 capacity, 5 workers)
   ↓ Process Execution
Docker Container Executor
   ↓ Stream Output
WebSocket Connection
   ↓ Real-time I/O
Interactive Terminal (Browser)

Request Flow

  1. Code Submission: Frontend sends code via REST API
  2. ID Generation: Backend assigns unique UUID to submission
  3. Queue Management: Submission enters job queue (max 100, 5 concurrent workers)
  4. WebSocket Connection: Frontend establishes WS connection with submission ID
  5. Container Execution: Code runs in isolated Docker container
  6. Output Streaming: Real-time stdout/stderr sent via WebSocket
  7. Interactive Input: User input captured and sent to running process
  8. Cleanup: Container auto-removed after execution

Container Isolation Strategy

Each code execution runs in a fresh, isolated Docker container with:

  • Memory limits: 100MB (Python, C, C++, JS, Go), 400MB (Java)
  • CPU limits: 10% of one core (most languages), 50% (Java)
  • Network isolation: --network=none - no internet access
  • Process limits: --pids-limit=20 - prevents fork bombs
  • Time limits: 30-second default execution timeout
  • Auto-removal: --rm flag ensures containers are cleaned up

Implementation Details

Monaco Editor Integration

Setting up the Monaco editor with multi-language support and file management:

import { Editor } from '@monaco-editor/react';
import { useState } from 'react';
 
const EditorArea = () => {
  const [activeFile, setActiveFile] = useState({
    name: 'main.py',
    content: 'print("Hello, World!")',
    language: 'python'
  });
 
  const handleEditorChange = (value) => {
    setActiveFile(prev => ({
      ...prev,
      content: value
    }));
  };
 
  return (
    <Editor
      height="100%"
      language={activeFile.language}
      value={activeFile.content}
      onChange={handleEditorChange}
      theme="vs-dark"
      options={{
        fontSize: 14,
        minimap: { enabled: false },
        scrollBeyondLastLine: false,
        automaticLayout: true,
        tabSize: 2,
        wordWrap: 'on',
      }}
    />
  );
};
 
export default EditorArea;

Code Execution API (Go Backend)

Backend endpoint for handling code submissions:

// Handler for code submission
func (h *Handler) SubmitCodeHandler(w http.ResponseWriter, r *http.Request) {
    // Parse request
    var submission models.CodeSubmission
    if err := json.NewDecoder(r.Body).Decode(&submission); err != nil {
        http.Error(w, "Invalid request format", http.StatusBadRequest)
        return
    }
 
    // Validate request
    if submission.Code == "" {
        http.Error(w, "Code cannot be empty", http.StatusBadRequest)
        return
    }
 
    if submission.Language == "" {
        http.Error(w, "Language must be specified", http.StatusBadRequest)
        return
    }
 
    // Generate ID if not provided
    if submission.ID == "" {
        submission.ID = uuid.New().String()
    }
 
    // Submit code for execution
    id := h.executor.SubmitCode(&submission)
 
    // Return response
    response := models.SubmissionResponse{
        ID:      id,
        Status:  "queued",
        Message: "Code submission accepted and queued for execution",
    }
 
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(response)
}

Docker Container Execution Service

Core execution service using Go's exec package:

package executor
 
import (
    "bytes"
    "context"
    "fmt"
    "os"
    "os/exec"
    "path/filepath"
    "time"
)
 
type CodeExecutor struct {
    config          *config.Config
    inputChannels   map[string]chan string
    inputMutex      sync.Mutex
    terminalClients map[string][]*websocket.Conn
    terminalMutex   sync.Mutex
}
 
// Execute Python code in Docker container
func (e *CodeExecutor) executePython(submission *models.CodeSubmission, tempDir string, langConfig config.LanguageConfig) {
    // Write code to file
    codeFile := filepath.Join(tempDir, "code"+langConfig.FileExt)
    if err := os.WriteFile(codeFile, []byte(submission.Code), 0644); err != nil {
        submission.Status = "failed"
        submission.Output = "Failed to write code file: " + err.Error()
        return
    }
 
    // Setup Docker run command with unbuffered Python output
    cmd := exec.Command(
        "docker", "run", "--rm", "-i",
        "--network=none",
        "--memory="+langConfig.MemoryLimit,
        "--cpu-quota="+fmt.Sprintf("%d", int(float64(100000)*0.1)), // 10% CPU
        "--pids-limit=20",
        "-v", tempDir+":/code",
        "-e", "PYTHONUNBUFFERED=1", // Force Python to be unbuffered
        langConfig.Image,
        "python", "-u", "/code/code.py", // -u for unbuffered I/O
    )
 
    // Execute with I/O handling
    e.executeWithIO(cmd, submission, time.Duration(langConfig.TimeoutSec)*time.Second)
}
 
// Execute Java code with compilation step
func (e *CodeExecutor) executeJava(submission *models.CodeSubmission, tempDir string, langConfig config.LanguageConfig) {
    // Extract class name from code
    className := extractJavaClassName(submission.Code)
    
    // Write code to file
    codeFile := filepath.Join(tempDir, className+langConfig.FileExt)
    if err := os.WriteFile(codeFile, []byte(submission.Code), 0644); err != nil {
        submission.Status = "failed"
        submission.Output = "Failed to write code file: " + err.Error()
        return
    }
 
    // Compile Java code
    compileCmd := exec.Command(
        "docker", "run", "--rm",
        "-v", tempDir+":/code",
        langConfig.Image,
        "javac", "/code/"+className+".java",
    )
    
    compileOutput, compileErr := compileCmd.CombinedOutput()
    if compileErr != nil {
        submission.Status = "failed"
        submission.Output = "Compilation error:\n" + string(compileOutput)
        e.sendToTerminals(submission.ID, models.NewOutputMessage(string(compileOutput), true))
        return
    }
    
    // Setup Docker run command for execution
    cmd := exec.Command(
        "docker", "run", "--rm", "-i",
        "--network=none",
        "--memory="+langConfig.MemoryLimit,
        "--cpu-quota="+fmt.Sprintf("%d", int(float64(100000)*0.5)), // 50% CPU
        "--pids-limit=20",
        "-v", tempDir+":/code",
        langConfig.Image,
        "java", "-XX:+TieredCompilation", "-XX:TieredStopAtLevel=1",
        "-Xms64m", "-Xmx256m",
        "-cp", "/code", className,
    )
 
    // Execute the code with input handling
    e.executeWithIO(cmd, submission, time.Duration(langConfig.TimeoutSec)*time.Second)
}
 
// Helper function to extract Java class name from code
func extractJavaClassName(code string) string {
    re := regexp.MustCompile(`public\s+class\s+(\w+)`)
    matches := re.FindStringSubmatch(code)
    if len(matches) > 1 {
        return matches[1]
    }
    return "Main"
}

WebSocket for Real-time I/O

WebSocket handler for streaming execution output and handling user input:

// WebSocket handler for terminal interaction
func (h *Handler) TerminalWebSocketHandler(w http.ResponseWriter, r *http.Request) {
    // Upgrade HTTP to WebSocket
    conn, err := upgrader.Upgrade(w, r, nil)
    if err != nil {
        log.Printf("WebSocket upgrade failed: %v", err)
        return
    }
    defer conn.Close()
 
    // Get submission ID from URL
    vars := mux.Vars(r)
    id := vars["id"]
 
    // Register this WebSocket connection
    h.executor.RegisterTerminal(id, conn)
    defer h.executor.UnregisterTerminal(id, conn)
 
    // Read messages from client (user input)
    for {
        var msg models.WebSocketMessage
        if err := conn.ReadJSON(&msg); err != nil {
            break
        }
 
        // Handle input message type
        if msg.Type == "input" {
            h.executor.SendInput(id, msg.Content)
        }
    }
}
 
// Execute with I/O handling through WebSocket
func (e *CodeExecutor) executeWithIO(cmd *exec.Cmd, submission *models.CodeSubmission, timeout time.Duration) {
    // Setup pipes for stdin, stdout, stderr
    stdin, _ := cmd.StdinPipe()
    stdout, _ := cmd.StdoutPipe()
    stderr, _ := cmd.StderrPipe()
 
    // Create input channel for this submission
    inputChan := make(chan string, 10)
    e.inputMutex.Lock()
    e.inputChannels[submission.ID] = inputChan
    e.inputMutex.Unlock()
 
    // Clean up when done
    defer func() {
        e.inputMutex.Lock()
        delete(e.inputChannels, submission.ID)
        e.inputMutex.Unlock()
        close(inputChan)
    }()
 
    // Start the command
    if err := cmd.Start(); err != nil {
        submission.Status = "failed"
        submission.Output = "Failed to start process: " + err.Error()
        return
    }
 
    // Output buffer to collect all output
    var outputBuffer bytes.Buffer
 
    // Send initial input if provided
    if submission.Input != "" {
        io.WriteString(stdin, submission.Input+"\n")
    }
 
    ctx, cancel := context.WithTimeout(context.Background(), timeout)
    defer cancel()
 
    // Handle stdout in a goroutine
    go func() {
        buffer := make([]byte, 1024)
        for {
            n, err := stdout.Read(buffer)
            if n > 0 {
                output := string(buffer[:n])
                outputBuffer.WriteString(output)
                
                // Send to all connected WebSocket clients
                e.sendToTerminals(submission.ID, models.NewOutputMessage(output, false))
            }
            if err != nil {
                break
            }
        }
    }()
 
    // Handle stderr in a goroutine
    go func() {
        buffer := make([]byte, 1024)
        for {
            n, err := stderr.Read(buffer)
            if n > 0 {
                output := string(buffer[:n])
                outputBuffer.WriteString(output)
                
                // Send error output to WebSocket clients
                e.sendToTerminals(submission.ID, models.NewOutputMessage(output, true))
            }
            if err != nil {
                break
            }
        }
    }()
 
    // Handle input from WebSocket
    go func() {
        for input := range inputChan {
            io.WriteString(stdin, input+"\n")
        }
    }()
 
    // Wait for completion or timeout
    done := make(chan error, 1)
    go func() {
        done <- cmd.Wait()
    }()
 
    select {
    case <-ctx.Done():
        cmd.Process.Kill()
        submission.Status = "failed"
        submission.Output = "Execution timeout"
    case err := <-done:
        if err != nil {
            submission.Status = "failed"
        } else {
            submission.Status = "completed"
        }
        submission.Output = outputBuffer.String()
    }
}

Frontend WebSocket Integration

Client-side WebSocket connection for real-time terminal interaction:

const runCode = async () => {
  setIsRunning(true);
  setTerminalOutput([{ type: 'output', content: 'Submitting code...' }]);
  
  try {
    // Close any existing socket
    if (activeSocket) {
      activeSocket.close();
      setActiveSocket(null);
    }
    
    // Submit the code to get an execution ID
    const apiUrl = import.meta.env.VITE_API_URL || 'http://localhost:8080';
    
    const submitResponse = await fetch(`${apiUrl}/api/submit`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        language: language,
        code: activeFile.content,
        input: ""
      }),
    });
    
    if (!submitResponse.ok) {
      throw new Error(`Server error: ${submitResponse.status}`);
    }
    
    const { id } = await submitResponse.json();
    
    // Connect to WebSocket for real-time output
    const wsUrl = `${apiUrl.replace('http', 'ws')}/ws/terminal?id=${id}`;
    const socket = new WebSocket(wsUrl);
    
    socket.onopen = () => {
      console.log('WebSocket connected');
      setActiveSocket(socket);
    };
    
    socket.onmessage = (event) => {
      const data = event.data;
      
      // Check if it's a JSON message (status/error) or plain text (output)
      try {
        const message = JSON.parse(data);
        
        if (message.type === 'output') {
          setTerminalOutput(prev => [...prev, {
            type: message.isError ? 'error' : 'output',
            content: message.content
          }]);
        } else if (message.type === 'status') {
          if (message.content === 'completed' || message.content === 'failed') {
            setIsRunning(false);
            socket.close();
          }
        }
      } catch {
        // Plain text output
        setTerminalOutput(prev => [...prev, {
          type: 'output',
          content: data
        }]);
      }
    };
    
    socket.onerror = (error) => {
      console.error('WebSocket error:', error);
      setTerminalOutput(prev => [...prev, {
        type: 'error',
        content: 'WebSocket connection error'
      }]);
      setIsRunning(false);
    };
    
    socket.onclose = () => {
      console.log('WebSocket closed');
      setActiveSocket(null);
      setIsRunning(false);
    };
    
  } catch (error) {
    setTerminalOutput(prev => [...prev, {
      type: 'error',
      content: error.message || 'Execution failed'
    }]);
    setIsRunning(false);
  }
};
 
// Send user input to WebSocket
const handleTerminalInput = (input) => {
  if (activeSocket && activeSocket.readyState === WebSocket.OPEN) {
    activeSocket.send(JSON.stringify({
      type: 'input',
      content: input
    }));
  }
};

VS Code-like File System

File Tree Navigation with localStorage Persistence

const FileTree = ({ onFileSelect }) => {
  const [fileStructure, setFileStructure] = useState(() => {
    const saved = localStorage.getItem('fileStructure');
    return saved ? JSON.parse(saved) : defaultStructure;
  });
 
  const [expandedFolders, setExpandedFolders] = useState(new Set());
 
  const handleFileClick = (file) => {
    onFileSelect(file);
  };
 
  const toggleFolder = (folderId) => {
    setExpandedFolders(prev => {
      const next = new Set(prev);
      if (next.has(folderId)) {
        next.delete(folderId);
      } else {
        next.add(folderId);
      }
      return next;
    });
  };
 
  useEffect(() => {
    localStorage.setItem('fileStructure', JSON.stringify(fileStructure));
  }, [fileStructure]);
 
  return (
    <div className="file-tree">
      {fileStructure.map(item => (
        <FileTreeItem
          key={item.id}
          item={item}
          level={0}
          expanded={expandedFolders.has(item.id)}
          onToggle={toggleFolder}
          onFileClick={handleFileClick}
        />
      ))}
    </div>
  );
};

Tab Management System

const EditorTabs = ({ tabs, activeTab, onTabChange, onTabClose }) => {
  return (
    <div className="editor-tabs">
      {tabs.map(tab => (
        <div
          key={tab.id}
          className={`tab ${activeTab === tab.id ? 'active' : ''}`}
          onClick={() => onTabChange(tab.id)}
        >
          <span className="tab-icon">{getFileIcon(tab.language)}</span>
          <span className="tab-name">{tab.name}</span>
          <button
            className="tab-close"
            onClick={(e) => {
              e.stopPropagation();
              onTabClose(tab.id);
            }}
          >
            ×
          </button>
        </div>
      ))}
    </div>
  );
};

Deployment with Docker

Multi-Stage Dockerfile

Complete production-ready Dockerfile:

# Stage 1: Build the React frontend
FROM node:18-alpine AS frontend-builder
WORKDIR /app/frontend
 
# Build argument for API URL
ARG VITE_API_URL=""
ENV VITE_API_URL=$VITE_API_URL
 
# Install dependencies
COPY Frontend/package.json Frontend/yarn.lock* ./
RUN yarn install --frozen-lockfile
 
# Copy source and build
COPY Frontend/ ./
RUN yarn build
 
# Stage 2: Build the Go backend
FROM golang:1.19-alpine AS backend-builder
WORKDIR /app/backend
 
# Install git for dependencies
RUN apk update && apk add --no-cache git
 
# Download Go dependencies
COPY new-backend/go.mod new-backend/go.sum ./
RUN go mod download
 
# Copy source and build
COPY new-backend/ ./
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -ldflags="-s -w" -o /monaco-backend .
 
# Stage 3: Final image with Nginx
FROM nginx:1.25-alpine
 
# Install Docker client for backend
RUN apk update && apk add --no-cache docker-cli
 
# Copy backend binary
COPY --from=backend-builder /monaco-backend /usr/local/bin/monaco-backend
 
# Copy frontend build
COPY --from=frontend-builder /app/frontend/dist /usr/share/nginx/html
 
# Copy Nginx config
COPY nginx.conf /etc/nginx/nginx.conf
 
# Expose port
EXPOSE 80
 
# Start both backend and Nginx
CMD sh -c 'monaco-backend & nginx -g "daemon off;"'

Nginx Configuration

events {
    worker_connections 1024;
}
 
http {
    include /etc/nginx/mime.types;
    default_type application/octet-stream;
 
    server {
        listen 80;
        server_name localhost;
 
        # Frontend - serve React build
        location / {
            root /usr/share/nginx/html;
            try_files $uri $uri/ /index.html;
        }
 
        # Backend API - proxy to Go server
        location /api/ {
            proxy_pass http://localhost:8080;
            proxy_http_version 1.1;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection 'upgrade';
            proxy_set_header Host $host;
            proxy_cache_bypass $http_upgrade;
        }
 
        # WebSocket - proxy to Go server
        location /ws/ {
            proxy_pass http://localhost:8080;
            proxy_http_version 1.1;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection "upgrade";
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_read_timeout 86400;
        }
    }
}

Cloudflare Tunnel Setup

For secure public access without exposing ports:

{
  "tunnel": "5d2682ef-0b5b-47e5-b0fa-ad48968ce016",
  "credentials-file": "/etc/cloudflared/credentials.json",
  "ingress": [
    {
      "hostname": "api.ishikabhoyar.tech",
      "service": "http://host.docker.internal:9090"
    },
    {
      "service": "http_status:404"
    }
  ],
  "protocol": "http2",
  "loglevel": "info"
}

Tunnel Architecture:

Internet
   ↓
Cloudflare Edge (api.ishikabhoyar.tech)
   ↓
Cloudflare Tunnel (in Docker)
   ↓
host.docker.internal:9090
   ↓
Go Backend (running locally)

Language Support & Configuration

Supported Languages

func getLanguageConfigs() map[string]LanguageConfig {
    return map[string]LanguageConfig{
        "python": {
            Name:        "Python",
            Image:       "python:3.9-alpine",
            MemoryLimit: "100m",
            CPULimit:    "0.1",
            TimeoutSec:  30,
            FileExt:     ".py",
        },
        "java": {
            Name:        "Java",
            Image:       "eclipse-temurin:11-jdk-alpine",
            MemoryLimit: "400m",
            CPULimit:    "0.5",
            TimeoutSec:  30,
            FileExt:     ".java",
        },
        "c": {
            Name:        "C",
            Image:       "gcc:latest",
            MemoryLimit: "100m",
            CPULimit:    "0.1",
            TimeoutSec:  30,
            FileExt:     ".c",
        },
        "cpp": {
            Name:        "C++",
            Image:       "gcc:latest",
            MemoryLimit: "100m",
            CPULimit:    "0.1",
            TimeoutSec:  30,
            FileExt:     ".cpp",
        },
        "javascript": {
            Name:        "JavaScript",
            Image:       "node:18-alpine",
            MemoryLimit: "100m",
            CPULimit:    "0.1",
            TimeoutSec:  30,
            FileExt:     ".js",
        },
        "golang": {
            Name:        "Go",
            Image:       "golang:1.21-alpine",
            MemoryLimit: "100m",
            CPULimit:    "0.1",
            TimeoutSec:  30,
            FileExt:     ".go",
        },
    }
}

Language-Specific Optimizations

Python: Unbuffered I/O for real-time output

"-e", "PYTHONUNBUFFERED=1",
"python", "-u", "/code/code.py"  // -u flag for unbuffered

Java: JVM optimization for faster startup

"java", 
"-XX:+TieredCompilation",     // Enable tiered compilation
"-XX:TieredStopAtLevel=1",    // Stop at C1 compiler
"-Xms64m", "-Xmx256m",        // Memory settings

C/C++: Line buffering with stdbuf

"stdbuf", "-oL", "-eL", "/code/program"  // Force line buffering

Security Measures

Container Isolation

All code execution happens in isolated Docker containers:

cmd := exec.Command(
    "docker", "run", "--rm", "-i",
    "--network=none",                    // No network access
    "--memory="+langConfig.MemoryLimit,  // Memory limit (100-400MB)
    "--cpu-quota="+cpuQuota,             // CPU limit (10-50%)
    "--pids-limit=20",                   // Max 20 processes
    "-v", tempDir+":/code",              // Mount code directory
    langConfig.Image,
    // ... execution command
)

Security Features:

  • Network disabled: --network=none prevents internet access
  • Resource limits: Memory, CPU, and process limits prevent abuse
  • Temporary directories: Each execution gets isolated temp directory
  • Auto-cleanup: --rm flag ensures containers are removed after execution
  • No persistent storage: Containers can't modify host filesystem

Job Queue Configuration

type ExecutorConfig struct {
    ConcurrentExecutions int  // Max: 100 concurrent jobs
    QueueCapacity        int  // Max: 1000 queued submissions
    DefaultTimeout       time.Duration  // Default: 30 seconds
}

Execution Timeouts

ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
 
select {
case <-ctx.Done():
    cmd.Process.Kill()
    submission.Status = "failed"
    submission.Output = "Execution timeout"
case err := <-done:
    // Execution completed
}

Configuration Management

Environment Variables

# Server Configuration
PORT=8080
READ_TIMEOUT=15
WRITE_TIMEOUT=15
IDLE_TIMEOUT=90
 
# Executor Configuration
CONCURRENT_EXECUTIONS=100
QUEUE_CAPACITY=1000
DEFAULT_TIMEOUT=30
 
# Sandbox Configuration
SANDBOX_NETWORK_DISABLED=true
SANDBOX_MEMORY_SWAP_LIMIT=0
SANDBOX_PIDS_LIMIT=50

CORS Configuration

corsHandler := cors.New(cors.Options{
    AllowedOrigins: []string{
        "http://localhost:5173",
        "https://monaco.ishikabhoyar.tech",
    },
    AllowedMethods: []string{
        "GET", "POST", "PUT", "DELETE", "OPTIONS",
    },
    AllowedHeaders: []string{
        "Content-Type", "Authorization",
    },
    AllowCredentials: true,
})

Challenges and Solutions

Challenge 1: Real-time Interactive Input

Problem: Programs requiring user input (like scanf in C, input() in Python) would hang indefinitely

Solution: Implemented WebSocket-based bidirectional communication

// Detect input prompts and notify frontend
func IsInputPrompt(text string) bool {
    patterns := []string{
        "enter", "input", "type", ":",
        "number", "name", "age", "value",
    }
    
    lowerText := strings.ToLower(text)
    for _, pattern := range patterns {
        if strings.Contains(lowerText, pattern) {
            return true
        }
    }
    return false
}
 
// Handle input from WebSocket
go func() {
    for input := range inputChan {
        io.WriteString(stdin, input+"\n")
    }
}()

Result: Users can interact with programs requiring input in real-time through the terminal

Challenge 2: Java Compilation and Execution

Problem: Java requires extracting class name and compiling before execution

Solution: Created two-step process with regex-based class name extraction

func extractJavaClassName(code string) string {
    re := regexp.MustCompile(`public\s+class\s+(\w+)`)
    matches := re.FindStringSubmatch(code)
    if len(matches) > 1 {
        return matches[1]
    }
    return "Main"
}
 
// Step 1: Compile
compileCmd := exec.Command(
    "docker", "run", "--rm",
    "-v", tempDir+":/code",
    langConfig.Image,
    "javac", "/code/"+className+".java",
)
 
// Step 2: Execute
execCmd := exec.Command(
    "docker", "run", "--rm", "-i",
    "-v", tempDir+":/code",
    langConfig.Image,
    "java", "-cp", "/code", className,
)

Challenge 3: Buffered Output Issues

Problem: Output from Python, C, and C++ programs wasn't appearing in real-time due to buffering

Solution: Language-specific unbuffering techniques

// Python: Use -u flag and PYTHONUNBUFFERED
"-e", "PYTHONUNBUFFERED=1",
"python", "-u", "/code/code.py"
 
// C/C++: Use stdbuf to force line buffering
"stdbuf", "-oL", "-eL", "/code/program"

Challenge 4: Container Cleanup

Problem: Failed executions left orphaned containers consuming resources

Solution: Implemented automatic cleanup with --rm flag and timeout handling

defer func() {
    // Ensure temporary directory is cleaned up
    if tempDir != "" {
        os.RemoveAll(tempDir)
    }
}()
 
// Timeout handling with process kill
select {
case <-ctx.Done():
    cmd.Process.Kill()
    submission.Status = "failed"
    submission.Output = "Execution timeout"
}

Challenge 5: Cloudflare Tunnel Configuration

Problem: Exposing local backend to internet securely without opening ports

Solution: Implemented Cloudflare Tunnel with separate tunnel-only mode

# Start backend on port 9090
PORT=9090 go run main.go
 
# Start tunnel in Docker (forwards to localhost:9090)
docker-compose -f docker-compose.tunnel-only.yml up

Tunnel Architecture Benefits:

  • No port forwarding needed
  • SSL/TLS handled by Cloudflare
  • DDoS protection from Cloudflare
  • Can run backend outside Docker for development

API Reference

REST Endpoints

POST /api/submit Submit code for execution

{
  "language": "python",
  "code": "print('Hello, World!')",
  "input": ""
}

Response:

{
  "id": "6423259c-ee14-c5aa-1c90-d5e989f92aa1",
  "status": "queued",
  "message": "Code submission accepted and queued for execution"
}

GET /api/status?id={id} Check execution status

Response:

{
  "id": "6423259c-ee14-c5aa-1c90-d5e989f92aa1",
  "status": "completed",
  "queuedAt": "2025-03-25T14:30:00Z",
  "startedAt": "2025-03-25T14:30:01Z",
  "completedAt": "2025-03-25T14:30:02Z",
  "executionTime": 1000
}

GET /api/result?id={id} Get complete execution result

Response:

{
  "id": "6423259c-ee14-c5aa-1c90-d5e989f92aa1",
  "status": "completed",
  "language": "python",
  "output": "Hello, World!",
  "executionTime": 1.5,
  "totalTime": 2.0
}

GET /api/queue-stats Get queue statistics

Response:

{
  "queue_stats": {
    "queue_length": 5,
    "max_workers": 100,
    "running_jobs": 3
  },
  "submissions": 42
}

WebSocket Endpoint

WS /ws/terminal?id={id}

Establishes real-time connection for terminal interaction.

Message Types:

// Output message (server → client)
{
  "type": "output",
  "content": "Hello, World!",
  "isError": false
}
 
// Input message (client → server)
{
  "type": "input",
  "content": "John Doe\n"
}
 
// Status message (server → client)
{
  "type": "status",
  "content": "completed"
}
 
// Error message (server → client)
{
  "type": "error",
  "content": "Execution failed"
}

Project Statistics

After building and deploying Monaco:

  • Lines of Code: 5,000+ (Frontend + Backend)
  • Supported Languages: 6 (Python, Java, C, C++, JavaScript, Go)
  • Concurrent Executions: Up to 100 simultaneous jobs
  • Queue Capacity: 1,000 submissions
  • Average Response Time: 1-3 seconds
  • Container Cleanup Rate: 100% (auto-removal)
  • Production Uptime: 99.9% with Cloudflare Tunnel

Lessons Learned

  1. Go's Concurrency Model is Perfect for This: Goroutines made handling concurrent WebSocket connections and Docker executions incredibly efficient. The job queue system scales beautifully.

  2. WebSocket vs Polling: Initially considered polling for results, but WebSockets provide much better UX for interactive programs and real-time output streaming.

  3. Language-Specific Buffering Matters: Each language handles I/O buffering differently. Python's -u flag, C's stdbuf, and Java's JVM settings are critical for real-time output.

  4. Container Cleanup is Non-Negotiable: Without --rm flag and proper timeout handling, orphaned containers can exhaust system resources quickly.

  5. Cloudflare Tunnel Simplifies Deployment: No need to configure port forwarding, SSL certificates, or deal with firewall rules. Tunnel handles it all securely.

  6. localStorage for File Persistence: Using browser localStorage for file structure persistence provides great UX without backend database complexity.

  7. Monaco Editor is Production-Ready: Microsoft's Monaco editor is incredibly powerful and well-documented. Same codebase as VS Code = reliability.

  8. Docker Socket Access is Powerful but Risky: Mounting /var/run/docker.sock gives backend full Docker control. Ensure proper security measures in production.

Tech Stack Highlights

Why Go for Backend?

  • Fast compilation and execution
  • Excellent concurrency with goroutines
  • Strong standard library for exec, HTTP, WebSockets
  • Low memory footprint compared to Node.js
  • Easy deployment (single binary)

Why Monaco Editor?

  • Same editor powering VS Code
  • Excellent TypeScript/JavaScript support
  • Built-in language features (syntax highlighting, IntelliSense)
  • Highly customizable and extensible
  • Active community and Microsoft backing

Why Docker for Execution?

  • Complete isolation for security
  • Easy resource limits (memory, CPU, network)
  • Consistent execution environment across languages
  • Simple cleanup with --rm flag
  • Widely adopted and well-documented

Future Enhancements

Planned Features:

  • Package Installation: Support for pip, npm, maven dependencies
  • Multi-file Projects: Handle projects with multiple source files
  • Collaborative Editing: Multiple users editing simultaneously (using OT/CRDT)
  • Code Templates: Pre-built templates for common algorithms
  • Test Cases: Run code against predefined test cases
  • Execution History: Save and restore previous code executions
  • Code Sharing: Generate shareable links for code snippets
  • GitHub Integration: Import/export code from GitHub repos
  • Custom Docker Images: User-defined execution environments

Performance Improvements:

  • Image Pre-pulling: Cache Docker images for faster startup
  • Connection Pooling: Reuse Docker connections
  • Result Caching: Cache execution results for identical code
  • CDN Integration: Serve static assets from CDN

Collaboration & Acknowledgments

This project was built in collaboration with Ishika Bhoyar, who contributed significantly to the frontend architecture, file system implementation, and UI/UX design. The seamless integration between Monaco editor and the terminal panel was a joint effort that required careful coordination between frontend and backend components.

Division of Work:

  • Arnab Bhowmik: Backend architecture, Docker execution engine, WebSocket implementation, API design, Cloudflare Tunnel setup, security measures
  • Ishika Bhoyar: Frontend architecture, Monaco editor integration, file tree navigation, tab management, terminal component, localStorage persistence, UI/UX design

Conclusion

Building Monaco was an incredible journey into containerized code execution, real-time WebSocket communication, and creating VS Code-like experiences in the browser. The project successfully combines modern web technologies with secure container isolation to provide a powerful online coding platform.

The Go backend proved to be an excellent choice for handling concurrent executions efficiently, while Docker provides the perfect isolation layer for running untrusted code safely. Monaco editor brings professional-grade code editing to the browser, and Cloudflare Tunnel simplifies secure deployment.

Whether you're building a coding interview platform, educational tool, or online IDE, these architectural patterns provide a solid foundation. The combination of REST APIs for submission, WebSockets for real-time I/O, and Docker for isolation creates a robust, scalable system.

Live Demo: Try Monaco at monaco.ishikabhoyar.tech

Source Code: Check out the project on GitHub

Have questions about containerized code execution or building online IDEs? Connect with me:

Let's build something amazing together! 🚀