From f20f5d594309a120d5968119412aecd0ba6656b1 Mon Sep 17 00:00:00 2001 From: R Ajay Kumar Date: Sun, 26 Jul 2026 22:40:22 +0530 Subject: [PATCH] unlock dev setup, add service healthchecks, fix compose mounts --- CONTRIBUTING.md | 39 +++++++++++------- admin/src/daemon/services/compose-command.ts | 4 ++ client/.env.example | 15 +++++++ docker/app/docker-compose.yaml | 43 ++++++++++++++++---- docker/judge0/docker-compose.yaml | 10 ++--- scripts/install.sh | 3 +- server/.env.example | 17 ++++++++ server/helpers/dbCon.js | 5 ++- server/index.js | 12 +++++- 9 files changed, 117 insertions(+), 31 deletions(-) create mode 100644 client/.env.example create mode 100644 server/.env.example diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1b937f0..29a29de 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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 diff --git a/admin/src/daemon/services/compose-command.ts b/admin/src/daemon/services/compose-command.ts index bfc3d65..da6ce6c 100644 --- a/admin/src/daemon/services/compose-command.ts +++ b/admin/src/daemon/services/compose-command.ts @@ -64,6 +64,9 @@ export class ComposeCommand { const env: Record = { ...process.env as Record, + // 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), @@ -113,6 +116,7 @@ export class ComposeCommand { const env: Record = { ...(process.env as Record), + 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", diff --git a/client/.env.example b/client/.env.example new file mode 100644 index 0000000..337cf97 --- /dev/null +++ b/client/.env.example @@ -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 diff --git a/docker/app/docker-compose.yaml b/docker/app/docker-compose.yaml index 8018ee9..972d5fa 100644 --- a/docker/app/docker-compose.yaml +++ b/docker/app/docker-compose.yaml @@ -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 @@ -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 @@ -18,6 +21,7 @@ services: build: context: . dockerfile: server/Dockerfile + restart: unless-stopped environment: - NODE_ENV=production - MONGODB_URI=${MONGODB_URI} @@ -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} @@ -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 @@ -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 diff --git a/docker/judge0/docker-compose.yaml b/docker/judge0/docker-compose.yaml index b3c0fc2..4b2e6e7 100644 --- a/docker/judge0/docker-compose.yaml +++ b/docker/judge0/docker-compose.yaml @@ -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} @@ -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} @@ -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: diff --git a/scripts/install.sh b/scripts/install.sh index 7ff68a9..7e7a63e 100644 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -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 @@ -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 diff --git a/server/.env.example b/server/.env.example new file mode 100644 index 0000000..7ae5bd2 --- /dev/null +++ b/server/.env.example @@ -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 diff --git a/server/helpers/dbCon.js b/server/helpers/dbCon.js index fe4f101..7cccbbc 100644 --- a/server/helpers/dbCon.js +++ b/server/helpers/dbCon.js @@ -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 }; diff --git a/server/index.js b/server/index.js index 9a60a85..6b562ec 100644 --- a/server/index.js +++ b/server/index.js @@ -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"); @@ -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);