Build Docs
A chronological walkthrough of how COMPILEME was built — from empty folder to a fully sandboxed online C++ compiler.
Chapter 1
Project Setup & Folder Structure
The first decision was the tech stack. The goal was a minimal, dependency-light prototype — no databases, no auth, no framework overhead.
- Backend: Node.js + Express — simple, fast to wire up
- Frontend: Plain HTML + vanilla JS — zero build step
- Execution: Docker (
gcc:latest) — every run is isolated
The resulting folder layout:
COMPILEME/
├── backend/
│ ├── server.js ← Express API
│ └── package.json
├── frontend/
│ └── index.html ← UI (editor + output)
├── docs/
│ └── index.html ← this page
├── Dockerfile ← containerises the backend
├── docker-compose.yml ← mounts Docker socket
└── README.md
Chapter 2
Backend — Express API
The backend is a single file: backend/server.js.
It exposes one endpoint and serves the frontend as static files.
Endpoint
POST /run
Content-Type: application/json
{ "code": "<C++ source>" }
→ 200 OK
{ "output": "<stdout + stderr>" }
Request validation
- Rejects missing or non-string
codefields immediately (400). - Rejects payloads larger than 50 KB before spawning Docker.
Child process
Node's built-in child_process.spawn launches docker run.
We use spawn (not exec) so we can pipe stdin without
shell quoting issues.
const proc = spawn('docker', dockerArgs);
proc.stdin.write(code); // send C++ source into container
proc.stdin.end();
Both stdout and stderr of the Docker process are collected
into a single string and returned as output.
Chapter 3
Docker Sandboxing
Every code submission gets its own brand-new container. There is no shared state.
Execution strategy
Instead of mounting host directories, the source code is piped via stdin:
bash -c 'cat > /tmp/main.cpp \
&& g++ /tmp/main.cpp -o /tmp/main 2>&1 \
&& timeout 2 /tmp/main 2>&1'
cat reads stdin and writes main.cpp. Then g++
compiles it. If compilation succeeds, timeout 2 ./main runs the binary
with a hard 2-second wall-clock limit.
Resource limits applied
| Flag | Value | What it prevents |
|---|---|---|
--memory=256m | 256 MB | Memory exhaustion / OOM attacks |
--memory-swap=256m | 256 MB (= memory) | Disables swap to enforce hard cap |
--cpus=1 | 1 core | CPU hogging / spinning loops |
--network=none | — | Any inbound or outbound network access |
--pids-limit=64 | 64 processes | Fork bombs |
timeout 2 | 2 s | Infinite loops, sleep() abuse |
--rm | — | Ensures container is deleted on exit |
Server-side hard timeout
A 15-second setTimeout in Node kills the Docker process with
SIGKILL if Docker itself stalls (e.g., image pull hangs, daemon unresponsive).
This prevents the HTTP request from hanging indefinitely.
gcc:latest (~1.2 GB).
Run docker pull gcc:latest once before starting the server.
Chapter 4
Frontend UI
A single frontend/index.html — no build step, no npm install, no framework.
Express serves it as a static file at /.
Key UI elements
-
Code editor — a
<textarea>styled to look like a terminal. Tab key is intercepted to insert 4 spaces instead of moving focus. - Run button — shows a CSS spinner while waiting; disabled during execution to prevent double-submit.
-
Output box — a
<pre>element. Border turns green on success, red if the output contains an error string.
fetch call
const res = await fetch('/run', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ code })
});
const data = await res.json();
outputEl.textContent = data.output;
Frontend and backend share the same origin (localhost:3300), so no CORS
configuration is needed.
Chapter 5
Security Decisions
No shell injection
The C++ code is never interpolated into a shell string. It is written to
proc.stdin as raw bytes. This means backticks, semicolons, dollar signs,
quotes — none of them can escape the container.
No host filesystem access
No directories are mounted from the host. The container's /tmp is ephemeral
and disappears with --rm.
No network from containers
--network=none means user code cannot make HTTP requests, call external
APIs, exfiltrate data, or connect back to an attacker.
Input size cap
Payloads over 50 KB are rejected before Docker is ever invoked. Express's
json({ limit: '100kb' }) handles the HTTP layer.
Chapter 6
Request Flow — End to End
clicks Run
JSON body
validates input
stdin = code
compile + run
collected
output field
displayed
- User writes C++ code and clicks Run.
- Browser sends
POST /runwith the code as a JSON string. - Express validates and size-checks the payload.
- Node spawns
docker run --rm -i gcc:latest bash -c '...'. - The C++ source is piped to
catinside the container → written to/tmp/main.cpp. g++compiles the file. Errors go to stderr, redirected to stdout via2>&1.- If compilation succeeds,
timeout 2 ./mainexecutes the binary. - All output (stdout + stderr) is collected by Node.
- Container exits and is auto-removed (
--rm). - Node returns
{ "output": "..." }— browser displays it.
Chapter 7
Running Locally
Prerequisites
- Node.js 18+ — nodejs.org
- Docker Desktop (Mac/Windows) or Docker Engine (Linux)
Option A — run directly on host (simplest)
# 1. Pull the gcc image once (saves time on first run)
docker pull gcc:latest
# 2. Install backend dependencies
cd backend && npm install && cd ..
# 3. Start the server
node backend/server.js
# 4. Open in browser
open http://localhost:3300
Option B — run via Docker Compose
# Build and start the containerised backend
docker-compose up --build
# Open in browser
open http://localhost:3300
http://localhost:3300/docs.
Environment variable
Override the default port with PORT:
PORT=8080 node backend/server.js