A blazing-fast, Judge0-compatible code execution engine built for the ChaiCode platform.
Part of the ChaiCode ecosystem - Home for Programmers
CodeBox is a self-hosted code execution service that powers the coding challenges and practice problems on ChaiCode. It securely runs user-submitted code in isolated environments and returns the results - just like LeetCode or HackerRank.
- Judge0 API Compatible - Drop-in replacement, works with existing integrations
- Dual Execution Engines - Firecracker microVMs (125ms) or Docker containers (500ms)
- Auto-Detection - Automatically picks the fastest available executor
- 5 Languages - Python, JavaScript, C, C++, Java (easily extensible)
- Batch Submissions - Run multiple test cases in one request
- Production Ready - Redis queue, Prometheus metrics, auto-SSL with Caddy
┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ ChaiCode │────▶│ CodeBox │────▶│ Redis │────▶│ Workers │
│ Frontend │ │ API │ │ Queue │ │ │
└──────────────┘ └──────────────┘ └──────────────┘ └──────┬───────┘
│
┌─────────────────┴─────────────────┐
│ │
┌──────▼──────┐ ┌───────▼──────┐
│ Firecracker │ │ Docker │
│ (microVM) │ OR │ (container) │
└──────┬──────┘ └───────┬──────┘
│ │
└─────────────┬─────────────────────┘
│
┌─────────────▼─────────────┐
│ Sandboxed Code Execution │
│ (isolated, time-limited) │
└───────────────────────────┘
Pick the row that matches your situation.
| Mode | You need | URL you end up with | TLS | Build? |
|---|---|---|---|---|
| Fastest path | A server + Docker | http://<server-ip> |
none | no — pulls from Docker Hub |
| 1. Local | A laptop with Docker | http://localhost:3000 |
none | yes |
| 2. Server, no domain | A server + its IP | http://<server-ip> |
none | yes |
| 3. Server + domain | A server + a DNS A record | https://codebox.example.com |
automatic | yes |
If you just want CodeBox running on a VPS and are not changing the code, use
the fastest path — one file, no clone, no build. Modes 2 and 3 build from
source and are otherwise identical to each other; the only difference between
them is whether DOMAIN is set in .env, and you can move from one to the
other later without rebuilding.
Runs the API, a worker and Redis on your machine using the Docker executor. No domain, no reverse proxy, no SSL.
Prerequisites: Docker Desktop, Node.js 20+
git clone https://github.com/hiteshchoudhary/Codebox.git
cd Codebox
npm install
# Build the language runtime images (python, node, gcc, java, ...)
./scripts/build-images.sh
# Start API + worker + Redis
docker compose up -d
curl http://localhost:3000/healthThe dev stack ships with the token dev-token already set:
curl -X POST "http://localhost:3000/submissions?wait=true" \
-H "Content-Type: application/json" \
-H "X-Auth-Token: dev-token" \
-d '{"source_code": "print(\"Chai aur Code!\")", "language_id": 71}'The API is on port 3000 directly — Caddy is not involved in this mode.
Prebuilt images are published to Docker Hub, and the production stack needs
no language images: the worker image already bundles gcc, JDK 17, Python,
Node and TypeScript, and isolate runs the code inside that container. So a
whole deployment is one file plus a .env.
On a fresh server with Docker installed:
mkdir -p /opt/codebox && cd /opt/codebox
# Grab the single-file compose
curl -fsSLO https://raw.githubusercontent.com/hiteshchoudhary/Codebox/main/docker-compose.hub.yml
mv docker-compose.hub.yml docker-compose.yml
# Generate secrets and write .env
cat > .env <<EOF
AUTH_TOKEN=$(openssl rand -hex 32)
METRICS_TOKEN=$(openssl rand -hex 16)
GRAFANA_PASSWORD=$(openssl rand -hex 16)
# Leave DOMAIN blank for plain HTTP on this server's IP.
# Set it to a domain with an A record here for automatic HTTPS.
DOMAIN=
# Size these to the server. WORKER_CPUS must not exceed its vCPU count.
WORKER_CPUS=1.5
WORKER_MEMORY=4G
WORKER_CONCURRENCY=2
EOF
docker compose up -dThen check it and grab your token:
curl http://localhost/health
grep AUTH_TOKEN .envdocker-compose.hub.yml carries the Caddy and Prometheus configs inline as
Compose configs, so there are genuinely no other files to copy. If you would
rather paste than curl, open the file on GitHub and paste it straight into
docker-compose.yml on the server — it needs Docker Compose v2.23 or newer
(docker compose version).
To upgrade later:
docker compose pull && docker compose up -dPin a release instead of tracking latest by adding CODEBOX_VERSION=v1.2.3
to .env.
This path uses the prebuilt images. Build from source instead — modes 2 and 3 below — when you have changed the code.
Use this when you have a VPS but no domain yet, or you are running CodeBox on a private network. CodeBox is served as plain HTTP on port 80 of the server's IP. No DNS record and no certificate are required.
ssh root@<server-ip>
git clone https://github.com/hiteshchoudhary/Codebox.git /opt/codebox
cd /opt/codebox
# Run setup with NO argument -> IP mode
./scripts/setup-production.shThe script installs Docker, opens the firewall, generates an API token, builds the images and starts everything. When it finishes it prints your URL and token.
Doing it by hand instead:
cp .env.example .env
# Set AUTH_TOKEN. Leave DOMAIN blank -- that is what selects IP mode.
nano .env
docker compose -f docker-compose.prod.yml up -d --buildVerify from your own machine:
curl http://<server-ip>/health
curl -X POST "http://<server-ip>/submissions?wait=true" \
-H "Content-Type: application/json" \
-H "X-Auth-Token: <your-token>" \
-d '{"source_code": "print(2+2)", "language_id": 71}'Note the trade-off. Without TLS, requests and your
X-Auth-Tokencross the network in cleartext. That is fine for a private network, an internal tool, or a staging box. Do not put a public production frontend on it — use mode 3 instead, which is a one-line change.
Same stack as mode 2, plus a certificate. Caddy obtains and renews it from Let's Encrypt automatically — there is no certbot step and no cron job.
Before you start, point an A record at the server and confirm it resolves:
dig +short codebox.example.com # must print your server's IPPorts 80 and 443 must both be reachable from the internet. Port 80 is not
optional — Let's Encrypt uses it to validate the domain. Check your provider's
panel firewall (Hostinger, DigitalOcean, AWS security groups) as well as ufw.
ssh root@<server-ip>
git clone https://github.com/hiteshchoudhary/Codebox.git /opt/codebox
cd /opt/codebox
# Pass the domain as the argument -> HTTPS mode
./scripts/setup-production.sh codebox.example.comDoing it by hand instead:
cp .env.example .env
# Set AUTH_TOKEN, and set DOMAIN=codebox.example.com
nano .env
docker compose -f docker-compose.prod.yml up -d --buildVerify:
curl https://codebox.example.com/healthHTTP is redirected to HTTPS automatically.
Nothing is rebuilt and no data is lost:
# 1. Point an A record at the server, wait for it to resolve
dig +short codebox.example.com
# 2. Set the domain
nano .env # DOMAIN=codebox.example.com
# 3. Recreate Caddy
docker compose -f docker-compose.prod.yml up -dCaddy picks up the domain, requests a certificate and starts serving HTTPS
within a few seconds. To go back to IP mode, blank out DOMAIN and re-run
the same command.
One variable. docker-compose.prod.yml turns DOMAIN into Caddy's site
address:
- SITE_ADDRESS=${DOMAIN:-:80}DOMAIN in .env |
Caddy site address | Result |
|---|---|---|
| blank or absent | :80 |
Serves HTTP on every IP and hostname. Caddy never requests a certificate and never binds 443. |
codebox.example.com |
codebox.example.com |
Binds 80 and 443, obtains a certificate, redirects HTTP to HTTPS. |
Caddy only attempts certificate issuance when the site address carries a hostname, so IP mode cannot fail on a DNS or ACME error.
| Load | Specs | Notes |
|---|---|---|
| Light (< 100 submissions/hr) | 2 vCPU, 4GB RAM | Set WORKER_CPUS=1.5, WORKER_MEMORY=3G |
| Medium (< 1000 submissions/hr) | 4 vCPU, 8GB RAM | Defaults in .env.example fit here |
| Heavy (> 1000 submissions/hr) | 8 vCPU, 16GB RAM | Raise WORKER_CPUS, WORKER_MEMORY, WORKER_CONCURRENCY |
WORKER_CPUSmust never exceed the host's vCPU count — Docker refuses to start the container withRange of CPUs is from 0.01 to N.00. All resource limits are tunable in.env; see the block at the bottom of.env.example.
For Firecracker support, choose a host with dedicated vCPUs and nested virtualisation (
/dev/kvmmust exist). Most shared VPS plans do not offer it — CodeBox falls back to isolate or Docker automatically.
Secure your droplet before deploying. Run these commands as root.
# Create user with sudo access
adduser deploy
usermod -aG sudo deploy
usermod -aG docker deploy
# Copy SSH keys to new user
mkdir -p /home/deploy/.ssh
cp ~/.ssh/authorized_keys /home/deploy/.ssh/
chown -R deploy:deploy /home/deploy/.ssh
chmod 700 /home/deploy/.ssh
chmod 600 /home/deploy/.ssh/authorized_keys# Edit SSH config
nano /etc/ssh/sshd_configUpdate these lines:
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
# Restart SSH
systemctl restart sshdWarning: Make sure you can login as
deployuser before disabling root!
# Allow only essential ports
ufw default deny incoming
ufw default allow outgoing
ufw allow 22/tcp # SSH
ufw allow 80/tcp # HTTP
ufw allow 443/tcp # HTTPS
ufw enableapt install unattended-upgrades
dpkg-reconfigure -plow unattended-upgradesapt install fail2ban
systemctl enable fail2ban
systemctl start fail2ban# Login as deploy user
ssh deploy@your-droplet-ip
# Clone and setup
sudo git clone https://github.com/chaicode/codebox.git /opt/codebox
sudo chown -R deploy:deploy /opt/codebox
cd /opt/codebox
# Run production setup
# With a domain (HTTPS):
./scripts/setup-production.sh api.yourdomain.com
# Or without one (plain HTTP on this server's IP):
./scripts/setup-production.sh- Non-root user created (
deploy) - SSH key authentication only
- Root login disabled
- Password login disabled
- Firewall enabled (UFW)
- Fail2Ban installed
- Auto-updates enabled
.github/workflows/publish-images.yml builds and pushes both images to Docker
Hub on every push to main, on every v* tag, and on manual dispatch:
| Image | From |
|---|---|
hiteshchoudhary/codebox-api |
docker/api/Dockerfile |
hiteshchoudhary/codebox-worker |
docker/worker/Dockerfile |
Tags pushed: latest (main), the git tag (v1.2.3), and the short SHA.
One-time setup. Add two repository secrets under Settings → Secrets and variables → Actions:
| Secret | Value |
|---|---|
DOCKERHUB_USERNAME |
your Docker Hub username |
DOCKERHUB_TOKEN |
an access token from hub.docker.com/settings/security, scope Read & Write — a token, not your password |
Both repositories must exist on Docker Hub (or the account must allow auto-creation) before the first push.
Images are built for linux/amd64 only. docker/worker/Dockerfile hardcodes
x86_64-linux-gnu and java-17-openjdk-amd64, and every mainstream VPS is
amd64 — but this means the published images will not run on an ARM server or
an Apple Silicon Mac.
| Method | Endpoint | Description |
|---|---|---|
POST |
/submissions |
Create submission (async) |
POST |
/submissions?wait=true |
Create and wait for result |
GET |
/submissions/:token |
Get result by token |
POST |
/submissions/batch |
Submit multiple (up to 20) |
GET |
/submissions/batch?tokens=a,b,c |
Get multiple results |
| Method | Endpoint | Description |
|---|---|---|
GET |
/health |
Health check |
GET |
/languages |
List supported languages |
GET |
/statuses |
List status codes |
GET |
/executor |
Show executor info |
GET |
/metrics |
Prometheus metrics |
| ID | Language | Image |
|---|---|---|
| 50 | C (GCC 9) | codebox/gcc:9 |
| 54 | C++ (GCC 9) | codebox/gcc:9 |
| 62 | Java (OpenJDK 17) | codebox/java:17 |
| 63 | JavaScript (Node 18) | codebox/node:18 |
| 71 | Python (3.8) | codebox/python:3.8 |
| ID | Description |
|---|---|
| 1 | In Queue |
| 2 | Processing |
| 3 | Accepted |
| 4 | Wrong Answer |
| 5 | Time Limit Exceeded |
| 6 | Compilation Error |
| 7-12 | Runtime Errors |
| 13 | Internal Error |
| Service | URL |
|---|---|
| API | http://localhost:3000 |
| Health Check | http://localhost:3000/health |
| Metrics | http://localhost:3000/metrics |
| Service | URL (mode 2, no domain) | URL (mode 3, with domain) |
|---|---|---|
| API | http://<server-ip> | https://your-domain.com |
| Health Check | http://<server-ip>/health | https://your-domain.com/health |
| Prometheus | http://localhost:9090 (SSH tunnel) | same |
| Grafana | http://localhost:3001 (SSH tunnel) | same |
Prometheus and Grafana are never published to the internet in either mode — reach them through an SSH tunnel.
Access Grafana via SSH tunnel:
ssh -L 3001:localhost:3001 user@your-server
# Then open http://localhost:3001| Variable | Default | Description |
|---|---|---|
AUTH_TOKEN |
- | Required. API authentication token. Compose refuses to start without it. |
DOMAIN |
blank | Blank = plain HTTP on the server IP. Set = automatic HTTPS for that domain. |
EXECUTOR_TYPE |
auto |
auto, isolate, docker, or firecracker |
WORKER_CONCURRENCY |
4 (dev) / 2 (prod) |
Parallel executions per worker |
WORKER_CPUS |
1.5 |
Must not exceed the host's vCPU count |
WORKER_MEMORY |
4G |
Worker container memory limit |
DEFAULT_CPU_TIME_LIMIT |
5 |
Seconds |
DEFAULT_MEMORY_LIMIT |
128000 |
KB |
See .env.example for all options.
- Sandboxed Execution - Each submission runs in isolation
- Network Disabled - No internet access from user code
- Resource Limits - CPU, memory, and process limits
- Time Limits - Prevents infinite loops
- Non-root - Code runs as unprivileged user
codebox/
├── src/
│ ├── api/ # Express routes & middleware
│ ├── executor/ # Docker & Firecracker executors
│ ├── queue/ # BullMQ job processing
│ ├── languages/ # Language configurations
│ └── utils/ # Config, logger, helpers
├── docker/
│ └── images/ # Language runtime Dockerfiles
├── scripts/
│ ├── setup.sh # Local setup
│ ├── setup-production.sh # Production setup
│ └── build-images.sh # Build Docker images
├── docker-compose.yml # Local development
├── docker-compose.prod.yml # Production with Caddy
└── Caddyfile # Auto-SSL config
Import these into Postman for testing:
postman_collection.json- Basic API testspostman_leetcode_tests.json- LeetCode-style test cases
We welcome contributions! Please check out the ChaiCode GitHub for guidelines.
MIT
Built with ☕ by ChaiCode
Home for Programmers