Features • Architecture • Running locally • Tests • Scaling • Deployment
CodeLeet is a LeetCode-style platform: pick a problem, write a solution in the browser, and get a verdict from a judge that runs your code in an isolated Docker sandbox. Results stream back live over WebSockets, along with how the run used time and memory. When a solution fails, the judge looks for a smaller input that fails the same way; when it passes, it estimates its time complexity.
The public beta runs at https://beta.codeleet.me.
- C++ and Java solutions, LeetCode style: you write
class Solution, the platform suppliesmain, input parsing and output. - Run and Submit. Run checks your code against the tests without saving; Submit judges it and saves the verdict to your history.
- Verdicts: Accepted, Wrong Answer, Runtime Error, Time Limit Exceeded, Memory Limit Exceeded and Compilation Error, with the failing test.
- Resource timeline: memory and CPU over time for the heaviest test, plus time and peak memory per test.
- Smaller failing input: after a wrong answer, crash or limit, the judge searches for a much smaller input that fails the same way and shows it with the expected and actual output.
- Time complexity: after Accepted, the solution is timed at growing input sizes and fitted to a class (O(n), O(n log n), ...) and compared with the intended one.
- Live updates over WebSockets: Running, the verdict, then the analysis results as they finish.
- Accounts: email and password, or Google sign-in. Dashboard with a 12-week activity heatmap and recent submissions.
- Problems filtered by difficulty and topic, with search and a command palette (⌘K). Light and dark themes.
graph TD
Browser["Browser (React)"]
Caddy["Caddy<br/>HTTPS"]
Gateway["gateway<br/>nginx: React app, /api and /ws proxy"]
API["api<br/>Express + WebSocket"]
Redis[("Redis<br/>queues, pub/sub, cache")]
Mongo[("MongoDB")]
SW["submission-worker"]
RW["run-worker"]
AW["analysis-worker"]
Sandbox["sandbox container per job<br/>cl-meter compiles, runs, measures"]
Browser --> Caddy --> Gateway --> API
API --> Mongo
API -->|"add job"| Redis
Redis -->|"take job"| SW
Redis -->|"take job"| RW
Redis -->|"take job"| AW
SW --> Sandbox
RW --> Sandbox
AW --> Sandbox
SW -->|"save verdict"| Mongo
SW -->|"publish result"| Redis
Redis -.->|"forward to the browser's socket"| API
How a submission is judged:
POST /api/submissionssaves the submission asin_queueand adds a job to thesubmission-queue(BullMQ on Redis). The API never runs user code.- A
submission-workertakes the job and starts one sandbox container through the host's Docker. Inside it, cl-meter (backend/docker/cl-meter.c) compiles the code once, runs every test case, measures each run from/procand enforces the time and memory limits per test. - The worker compares outputs outside the sandbox (expected outputs never enter it), saves the verdict to MongoDB and publishes it on Redis.
- The API replica holding the browser's WebSocket forwards it. The latest message is also cached briefly in Redis, so a socket that connects late still gets it.
- Follow-ups arrive on the same socket: the smaller failing input (after a
failure), and the complexity estimate from the
analysis-worker(after Accepted, on its own low-priority queue).
Run uses the same path with its own queue and worker, and saves nothing.
- Frontend: React 19, Vite, Tailwind CSS 4, Monaco Editor, Framer Motion, React Router
- Backend: Node.js 22, Express 5, MongoDB with Mongoose, BullMQ and
ioredis,
wsfor WebSockets, Passport (Google OAuth), JWT - Judge: Docker sandboxes (GCC, Eclipse Temurin JDK 21) with
cl-meter, a small C program that runs and measures each test - Infrastructure: Docker Compose, nginx, Caddy, GitHub Actions, GitHub Container Registry
backend/
app.js, server.js, wsServer.js API: routes, health checks, WebSocket server
controllers/, routes/, models/ HTTP handlers, Express routes, Mongoose models
queue/ BullMQ queues: submission, run, analysis
worker/ submission, run and analysis workers
docker/ judge.js, runCpp.js, runJava.js, cl-meter.c, sandbox Dockerfiles
problemkit/ problem packages: validation, code generation, seeding,
smaller-input search, complexity fitting (see its README)
tests/ Jest tests
client/ React app, nginx.conf for the gateway
deploy/ Caddyfile, server setup and deploy scripts (see its README)
docker-compose.yml local stack
docker-compose.prod.yml production overrides
You need Docker (Docker Desktop on Mac or Windows). Everything else runs in containers.
# 1. Settings: set JWT_SECRET; Google sign-in is optional locally
cp .env.example .env
mkdir -p /tmp/codeleet-jobs
# 2. Sandbox images the workers start for each job
docker compose --profile sandbox build
# 3. Everything else: MongoDB, Redis, API, workers, gateway
docker compose up --build -dOpen http://localhost:8080. Stop with docker compose stop.
Problems are built from problem packages: a folder per problem with the
statement, tests, a reference solution and an input generator. The seed
script checks each package, generates the large tests, runs the reference in
the sandbox to get their expected outputs, and writes the problems to MongoDB.
Packages hold hidden tests and reference solutions, so they are kept out of
this repository (backend/problems/ is ignored). With packages in that
folder:
docker compose run --rm -v "$PWD/backend/problems:/app/problems:ro" \
submission-worker npm run seed:problemsThe package format and the seed options are described in
backend/problemkit/README.md.
cd backend && npm test # Jest, with an in-memory MongoDB
cd client && npm test # VitestCI (.github/workflows/ci.yml) runs both, builds every image and checks the
nginx config on each pull request and push to main.
With Docker Compose, every service scales by running more containers:
docker compose up -d --scale api=3 --scale submission-worker=2- API: the gateway (
client/nginx.conf) re-resolvesapievery 10 seconds and sends each request to the replica with the fewest active connections. A WebSocket can land on any replica, since verdicts reach it through Redis pub/sub.GET /api/healthnames the replica that answered;GET /api/readyreturns 503 until that replica reaches MongoDB and Redis, and the container healthcheck uses it. - Judging:
WORKER_CONCURRENCY(default 1) is how many jobs each submission or run worker handles at once. Every job starts a sandbox limited to one CPU, so keep (submission + run workers) ×WORKER_CONCURRENCY, plus the analysis worker, within the machine's cores. Past that, runs slow down and correct solutions start hitting the time limit. - Analysis: keep one
analysis-worker, so complexity timings don't compete with each other.
In production, set API_REPLICAS, SUBMISSION_WORKER_REPLICAS,
RUN_WORKER_REPLICAS, WORKER_CONCURRENCY and ANALYSIS_WORKER_REPLICAS in
.env (see .env.prod.example).
Production runs docker-compose.yml with docker-compose.prod.yml on one
Linux server: Caddy (automatic HTTPS), the gateway, the API, the workers and
Redis, with MongoDB on Atlas.
- A push to
mainruns CI. When it passes, every image is built for amd64 and arm64 and pushed to GHCR, taggedlatestand with the commit SHA. - The deploy job waits for approval in the GitHub
productionenvironment, then copies the compose files to the server over SSH and runsdeploy/deploy.sh <sha>: pin the tag, pull the images, restart what changed, wait for/api/ready, remove old images. - To roll back, run the Deploy workflow by hand with an earlier commit SHA.
docker compose up -d recreates containers, so the site is unavailable for a
few seconds during a deploy. Queued jobs stay in Redis, and workers finish
their current job before stopping.
Server setup, settings and secrets are in deploy/README.md.
Each judging job runs in a fresh container that is removed afterwards:
--network none: no network access.--user nobody,--cap-drop ALL,--security-opt no-new-privileges: an unprivileged user with no Linux capabilities.--read-only, with the job folder mounted read-only and a 64 MB/tmpfor compiling and running.--cpus 1,--memory 320mwith no swap,--pids-limit 64, and limits on open files and file size.- cl-meter enforces the time limit and a 256 MB memory limit per test, and kills anything the program leaves running.
- Only test inputs are given to the sandbox; outputs are compared outside it. Hidden tests, drivers and reference solutions are never returned by the API.