Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 24 additions & 15 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,31 +22,40 @@ cp server/.env.example server/.env
cp client/.env.example client/.env
```

Review the `.env` files and populate any required variables (e.g., `AUTH_SECRET`).
All three are required:

### Step 2: Start Infrastructure Dependencies
- **`.env`** (root) — read by Docker Compose. Supplies `POSTGRES_PASSWORD` and
`REDIS_PASSWORD` to the Judge0 stack; without it those services start with empty
credentials and fail to authenticate.
- **`server/.env`** — needs `MONGODB_URI` (the API exits on boot without it) and
`AUTH_SECRET` (authenticated requests return HTTP 500 without it).
- **`client/.env`** — needs `BACKEND_URL=http://localhost:8080`. The built-in default is
`http://server:8080`, a Docker-network name that does not resolve when the client runs
on the host.

Start the MongoDB database and Judge0 execution environment using Docker Compose from the root directory.
Set `AUTH_SECRET` to the **same** value in `server/.env` and `client/.env`; the client
signs session tokens and the server verifies them with that key. Generate one with
`openssl rand -hex 32`.

```bash
docker compose --project-name pomelo \
--env-file .env \
-f docker/app/docker-compose.dev.yaml \
-f docker/judge0/docker-compose.dev.yaml \
--project-directory . \
up mongo judge0-server judge0-workers seed -d
```
### Step 2: Start the Application

### Step 3: Start the Application

Install the monorepo dependencies and start the development servers.
Install the monorepo dependencies and start everything.

```bash
pnpm install
pnpm dev
```

The client will typically be available at `http://localhost:3000` and the server at `http://localhost:8080`.
`pnpm dev` brings up the infrastructure containers (MongoDB, Judge0 server and workers)
via Docker Compose, builds `@pomelo/code-gen`, then runs the client and server on the
host. It tears the containers back down when you exit.

The client will typically be available at `http://localhost:3000` and the server at
`http://localhost:8080`. Verify the API with:

```bash
curl localhost:8080/health
```

## Pull Request Process

Expand Down
4 changes: 4 additions & 0 deletions admin/src/daemon/services/compose-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ export class ComposeCommand {

const env: Record<string, string> = {
...process.env as Record<string, string>,
// Bind mounts in the compose files resolve against this. It must be the
// install root we were actually started with, not the /opt/pomelo default.
POMELO_ROOT: paths.root,
DOMAIN: domain,
PROTOCOL: protocol,
CADDY_HTTP_PORT: String(caddyHttp),
Expand Down Expand Up @@ -113,6 +116,7 @@ export class ComposeCommand {

const env: Record<string, string> = {
...(process.env as Record<string, string>),
POMELO_ROOT: paths.root,
// Activate ALL profiles during teardown so that previously active
// internal containers get properly stopped/removed.
COMPOSE_PROFILES: "internal-db,internal-judge0",
Expand Down
15 changes: 15 additions & 0 deletions client/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Next.js client — host development (`pnpm dev`)
#
# Copy to client/.env. In production these values are supplied by
# /opt/pomelo/config/app.env, not by this file.

# Must be IDENTICAL to AUTH_SECRET in server/.env.
# Generate one with: openssl rand -hex 32
AUTH_SECRET=dev-secret-change-me

# REQUIRED on the host. client/lib/env.ts defaults to http://server:8080,
# which is a Docker-network name that does not resolve outside Docker.
BACKEND_URL=http://localhost:8080

DOMAIN=localhost:3000
PROTOCOL=http
43 changes: 35 additions & 8 deletions docker/app/docker-compose.yaml
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# Bind mounts resolve against ${POMELO_ROOT}, which the daemon sets from its own
# install root (APP_ROOT at install time). Keep the default in sync with
# scripts/install.sh and admin/src/daemon/core/dev.ts.
services:
mongo:
image: mongo:7.0
Expand All @@ -6,7 +9,7 @@ services:
ports:
- "127.0.0.1:27017:27017"
volumes:
- /opt/pomelo/data/database/mongo:/data/db
- ${POMELO_ROOT:-/opt/pomelo}/data/database/mongo:/data/db
healthcheck:
test: ["CMD", "mongosh", "--eval", "db.adminCommand('ping')"]
interval: 10s
Expand All @@ -18,6 +21,7 @@ services:
build:
context: .
dockerfile: server/Dockerfile
restart: unless-stopped
environment:
- NODE_ENV=production
- MONGODB_URI=${MONGODB_URI}
Expand All @@ -31,13 +35,24 @@ services:
condition: service_healthy
required: false
volumes:
- /opt/pomelo/data:/app/data
- /opt/pomelo/config:/app/config:ro
- ${POMELO_ROOT:-/opt/pomelo}/data:/app/data
- ${POMELO_ROOT:-/opt/pomelo}/config:/app/config:ro
healthcheck:
test:
- CMD
- node
- -e
- "fetch('http://127.0.0.1:8080/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"
interval: 10s
timeout: 5s
retries: 5
start_period: 30s

client:
build:
context: .
dockerfile: client/Dockerfile
restart: unless-stopped
environment:
- NODE_ENV=production
- AUTH_SECRET=${AUTH_SECRET}
Expand All @@ -47,7 +62,17 @@ services:
- PROTOCOL=${PROTOCOL:-http}
depends_on:
server:
condition: service_started
condition: service_healthy
healthcheck:
test:
- CMD
- node
- -e
- "fetch('http://127.0.0.1:3000/').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"
interval: 10s
timeout: 5s
retries: 5
start_period: 30s

caddy:
image: caddy:2-alpine
Expand All @@ -56,8 +81,10 @@ services:
- "${CADDY_HTTP_PORT:-80}:80"
- "${CADDY_HTTPS_PORT:-443}:443"
volumes:
- /opt/pomelo/config/Caddyfile:/etc/caddy/Caddyfile:ro
- /opt/pomelo/runtime/logs:/var/log/caddy
- ${POMELO_ROOT:-/opt/pomelo}/config/Caddyfile:/etc/caddy/Caddyfile:ro
- ${POMELO_ROOT:-/opt/pomelo}/runtime/logs:/var/log/caddy
depends_on:
- client
- server
client:
condition: service_healthy
server:
condition: service_healthy
10 changes: 5 additions & 5 deletions docker/judge0/docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ services:
image: officialsosc/judge0:latest
profiles: ["internal-judge0"]
volumes:
- /opt/pomelo/config/judge0.conf:/judge0.conf:ro
- ${POMELO_ROOT:-/opt/pomelo}/config/judge0.conf:/judge0.conf:ro
env_file:
- /opt/pomelo/config/judge0.conf
- ${POMELO_ROOT:-/opt/pomelo}/config/judge0.conf
environment:
REDIS_PASSWORD: ${REDIS_PASSWORD}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
Expand All @@ -27,9 +27,9 @@ services:
command: ["./scripts/workers"]
profiles: ["internal-judge0"]
volumes:
- /opt/pomelo/config/judge0.conf:/judge0.conf:ro
- ${POMELO_ROOT:-/opt/pomelo}/config/judge0.conf:/judge0.conf:ro
env_file:
- /opt/pomelo/config/judge0.conf
- ${POMELO_ROOT:-/opt/pomelo}/config/judge0.conf
environment:
REDIS_PASSWORD: ${REDIS_PASSWORD}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
Expand All @@ -44,7 +44,7 @@ services:
image: postgres:16.2
profiles: ["internal-judge0"]
env_file:
- /opt/pomelo/config/judge0.conf
- ${POMELO_ROOT:-/opt/pomelo}/config/judge0.conf
environment:
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
Expand Down
3 changes: 2 additions & 1 deletion scripts/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ if [[ "$ACTION" == "uninstall" ]]; then
# Teardown Docker containers
if command -v docker &>/dev/null && [[ -f "$APP_ROOT/app/docker/app/docker-compose.yaml" ]]; then
log_info "Stopping Docker containers and removing ephemeral volumes..."
docker compose --project-name pomelo \
POMELO_ROOT="$APP_ROOT" docker compose --project-name pomelo \
-f "$APP_ROOT/app/docker/app/docker-compose.yaml" \
-f "$APP_ROOT/app/docker/judge0/docker-compose.yaml" \
down -v >/dev/null 2>&1 || true
Expand Down Expand Up @@ -539,6 +539,7 @@ elif [[ -f "$APP_ROOT/app/admin/bin/pomelod" ]]; then
# Fallback: start daemon directly (non-persistent)
log_warn "systemd unavailable — starting daemon in background (non-persistent)."
export POMELO_ROOT="$APP_ROOT"
export NODE_ENV=production
"$APP_ROOT/app/admin/bin/pomelod" --daemon --root "$APP_ROOT"

sleep 1
Expand Down
17 changes: 17 additions & 0 deletions server/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Backend server — host development (`pnpm dev`)
#
# Copy to server/.env. In production these values are supplied by
# /opt/pomelo/config/app.env, not by this file.

PORT=8080

# Mongo runs in Docker (`pnpm dev:infra`) and publishes 27017 on the host.
MONGODB_URI=mongodb://localhost:27017/pomelo

# Must be IDENTICAL to AUTH_SECRET in client/.env — the client signs the
# session token and the server verifies it with the same key.
# Generate one with: openssl rand -hex 32
AUTH_SECRET=dev-secret-change-me

# Judge0 runs in Docker and publishes 2358 on the host.
JUDGE0_URL=http://localhost:2358
5 changes: 4 additions & 1 deletion server/helpers/dbCon.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,7 @@ const connectDB = async () => {
}
};

module.exports = { connectDB };
// readyState 1 === connected. Used by the /health endpoint and the container healthcheck.
const isConnected = () => mongoose.connection.readyState === 1;

module.exports = { connectDB, isConnected };
12 changes: 11 additions & 1 deletion server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const app = express();
// Trust proxy for express-rate-limit (essential in production behind LB/proxy)
app.set("trust proxy", 1);

const { connectDB } = require("./helpers/dbCon");
const { connectDB, isConnected } = require("./helpers/dbCon");

// const compRoutes = require("./routes/compilerRoutes");
const contestRoutes = require("./routes/contestRoutes");
Expand Down Expand Up @@ -35,6 +35,16 @@ app.get("/", (req, res) => {
});
});

// Readiness probe — used by the container healthcheck and by Caddy's depends_on gate.
app.get("/health", (req, res) => {
const db = isConnected();
res.status(db ? 200 : 503).json({
status: db ? "ok" : "degraded",
db: db ? "up" : "down",
uptime: Math.round(process.uptime()),
});
});

// Routes

app.use("/api/auth", authRoutes);
Expand Down