From 2a6bed386c4a10f6b51858909007ff8719e6a57d Mon Sep 17 00:00:00 2001 From: ION606 Date: Wed, 10 Sep 2025 16:26:35 -0400 Subject: [PATCH] initial commit (no RAG server) --- browser/Dockerfile | 41 +++++ cloudflared-tunnel-config.yml | 19 +++ coderunner/Dockerfile | 31 ++++ coderunner/index.ts | 277 ++++++++++++++++++++++++++++++++++ coderunner/package-lock.json | 29 ++++ coderunner/package.json | 5 + docker-compose.yml | 119 +++++++++++++++ searxng.yml | 12 ++ 8 files changed, 533 insertions(+) create mode 100644 browser/Dockerfile create mode 100644 cloudflared-tunnel-config.yml create mode 100644 coderunner/Dockerfile create mode 100644 coderunner/index.ts create mode 100644 coderunner/package-lock.json create mode 100644 coderunner/package.json create mode 100644 docker-compose.yml create mode 100644 searxng.yml diff --git a/browser/Dockerfile b/browser/Dockerfile new file mode 100644 index 0000000..610f23a --- /dev/null +++ b/browser/Dockerfile @@ -0,0 +1,41 @@ +# minimal backend for deepresearch: browser-use web-ui on :7788 +FROM python:3.11-slim + +ENV PIP_NO_CACHE_DIR=1 \ + PYTHONUNBUFFERED=1 \ + PLAYWRIGHT_BROWSERS_PATH=/ms-playwright \ + WEBUI_IP=0.0.0.0 \ + WEBUI_PORT=7788 + +RUN apt-get update && apt-get install -y --no-install-recommends \ + git curl ca-certificates xvfb dumb-init \ + && rm -rf /var/lib/apt/lists/*; + +# grab upstream repo +WORKDIR /opt +RUN git clone --depth=1 https://github.com/browser-use/web-ui.git + +# create app user with uid=1000 to match compose +RUN useradd -u 1000 -ms /bin/bash appuser; + +WORKDIR /opt/web-ui + +# python deps + playwright browsers +RUN pip install --upgrade pip \ + && pip install -r requirements.txt \ + && python -m playwright install chromium --with-deps; + +# prepare writable paths the app expects +RUN mkdir -p /opt/web-ui/tmp /data && chown -R appuser:appuser /opt/web-ui /data + +USER appuser + +# copy default env +COPY .env .env + +EXPOSE 7788 +HEALTHCHECK --interval=30s --timeout=5s --retries=5 \ + CMD python -c "import urllib.request; urllib.request.urlopen('http://127.0.0.1:7788').read()" || exit 1 + +ENTRYPOINT ["dumb-init", "--"] +CMD ["python", "webui.py", "--ip", "0.0.0.0", "--port", "7788"] diff --git a/cloudflared-tunnel-config.yml b/cloudflared-tunnel-config.yml new file mode 100644 index 0000000..95867ff --- /dev/null +++ b/cloudflared-tunnel-config.yml @@ -0,0 +1,19 @@ +tunnel: ID +credentials-file: /etc/cloudflared/ID.json +ingress: + # The ollama instance + - hostname: mlep.domain.com + service: http://ollama-ip:11434 + originRequest: + httpHostHeader: "localhost" + + # The OpenWebUI instance + - hostname: owebui.domain.com + service: http://localhost:4000 + originRequest: + httpHostHeader: "localhost" + + # The tools instance + - hostname: owebtools.domain.com + service: http://mcp-server-ip:PORT + - service: http_status:404 diff --git a/coderunner/Dockerfile b/coderunner/Dockerfile new file mode 100644 index 0000000..bb8332f --- /dev/null +++ b/coderunner/Dockerfile @@ -0,0 +1,31 @@ +# syntax=docker/dockerfile:1 +FROM oven/bun:1.2.2-alpine + +WORKDIR /app + +# install docker cli + tini; no daemon, just the client +RUN apk add --no-cache docker-cli tini curl; + +# ----- map container 'docker' group to host docker.sock GID ----- +# pass the host's docker.sock GID at build time: --build-arg DOCKER_GID=$(stat -c '%g' /var/run/docker.sock) +ARG DOCKER_GID=977 +# create (or reuse) a group with that GID, then add the existing 'bun' user to it +RUN addgroup -g "${DOCKER_GID}" -S docker || true \ + && addgroup bun docker; + +# switch to the nonroot bun user (already default in the base image, but explicit is nice) +USER bun + +# your app +COPY index.ts ./index.ts + +# expose your tool server +EXPOSE 8787 +ENV PORT=8787 +# default docker host path; adjust if you mount elsewhere +ENV DOCKER_HOST=unix:///var/run/docker.sock + +# pid 1 -> tini +ENTRYPOINT ["/sbin/tini","--"] +CMD ["bun","index.ts"] + diff --git a/coderunner/index.ts b/coderunner/index.ts new file mode 100644 index 0000000..4450430 --- /dev/null +++ b/coderunner/index.ts @@ -0,0 +1,277 @@ +import http from "node:http"; +import { spawn } from "node:child_process"; + +const PORT = Number(process.env.PORT || 8787); +const HOST = "0.0.0.0"; + +/** + * @example const imgObj = { image: "gcc:14", installcommands: [aptUpdateAndInstall("gpp")], filename: "main.cpp", run: ... }, + */ +const aptUpdateAndInstall = (pkgs: string[] | string) => { + const istr = typeof pkgs === 'string' ? pkgs : pkgs.join(" "); + + return `apt-get update \ + && apt-get install -y --no-install-recommends ${istr} \ + && rm -rf /var/lib/apt/lists/*;` +} + +// allow-listed images and run commands per language +const LANGS = { + python: { image: "python:3.12-alpine", filename: "main.py", run: ["python", "main.py"] }, + node: { image: "node:22-alpine", filename: "main.mjs", run: ["node", "main.mjs"] }, + bun: { image: "oven/bun:1.2.2-alpine", filename: "main.ts", run: ["bun", "run", "main.ts"] }, + bash: { image: "alpine:3.20", filename: "main.sh", run: ["sh", "main.sh"] }, + ruby: { image: "ruby:3.3-alpine", filename: "main.rb", run: ["ruby", "main.rb"] }, + go: { image: "golang:1.22-alpine", filename: "main.go", run: ["go run main.go"] }, + rust: { image: "rust:1-alpine", filename: "main.rs", run: ["rustc -O main.rs -o main && ./main"] }, + java: { image: "eclipse-temurin:21-jdk", filename: "Main.java", run: ["javac Main.java && java Main"] }, + c: { image: "gcc:14", filename: "main.c", run: ["gcc -O2 main.c -o main.out && ./main.out"] }, + cpp: { image: "gcc:14", filename: "main.cpp", run: ["g++ -O2 main.cpp -o main.out && ./main.out"] }, +}; + +type langObj = { + image: string, + filename: string, + run: string[], + installcommands?: string[] +} + +type fileType = { + path: string, + content: string +} + +// docker binary (or set DOCKER_BIN=podman) +const DOCKER_BIN = process.env.DOCKER_BIN || "docker"; + +// basic openapi for open webui +const OPENAPI = { + openapi: "3.1.0", + info: { + title: "Container Code Runner", + version: "1.0.0", + description: + "run source code inside a sandboxed container. important: provide pure source code only; do not wrap code in shell commands or pipelines." + }, + paths: { + "/execute": { + post: { + operationId: "execute", + summary: "Run code in a sandboxed container", + // the model sees this text + description: + "use the language directly, not bash + the language. e.g., `#include...` (good) vs `echo '#include...' && gcc` (bad). pass only pure source text in `code`.", + requestBody: { + required: true, + content: { + "application/json": { + schema: { + type: "object", + properties: { + language: { + type: "string", + enum: Object.keys(LANGS), + description: + "the programming language to run. do not use 'bash' to wrap or invoke compilers/interpreters; select the actual language (e.g., 'c', 'cpp', 'python')." + }, + code: { + type: "string", + description: + "pure source code only. do not include shell commands, redirections, pipes, or `echo`/`printf` wrappers. examples: good: `print('hi')`; bad: `echo \"print('hi')\" | python`." + }, + args: { type: "array", items: { type: "string" } }, + files: { + type: "array", + items: { + type: "object", + properties: { + path: { type: "string" }, + content: { type: "string" } + }, + required: ["path", "content"], + description: + "optional supporting files. contents must be pure file text, not shell commands." + } + } + }, + required: ["language", "code"] + } + } + } + }, + responses: { + "200": { + description: "Execution result", + content: { + "application/json": { + schema: { + type: "object", + properties: { + stdout: { type: "string" }, + stderr: { type: "string" }, + exitCode: { type: "integer" }, + timedOut: { type: "boolean" } + } + } + } + } + } + } + } + } + } +}; + + +function sendJson(res, status, obj) { + const body = JSON.stringify(obj); + res.writeHead(status, { "content-type": "application/json; charset=utf-8" }); + res.end(body); +} + +async function ensureImage(spec: langObj) { + // note: 'docker run' has a --pull policy (missing|always|never), + const { spawn } = await import("node:child_process"), + DOCKER_BIN = process.env.DOCKER_BIN || "docker"; + + // check if image exists locally + const inspect = spawn(DOCKER_BIN, ["image", "inspect", spec.image], { stdio: "ignore" }), + ok = await new Promise((r) => inspect.on("close", (c) => r(c === 0))); + if (ok) return; + + // pull with a bigger timeout the first time (4 minutes) + await new Promise((resolve) => { + const child = spawn(DOCKER_BIN, ["image", "pull", "--quiet", spec.image]); + let timer = setTimeout(() => { + child.kill("SIGKILL"); + resolve(new Error("pull timeout")); + }, 240_000); + + child.on("close", (code) => { + clearTimeout(timer); + resolve(code === 0 ? null : new Error(`pull failed: ${code}`)); + }); + }).then((err) => { + if (err) throw err; + }); + + // // dependancies + // if (spec.installcommands) { + // const commandFull = spec.installcommands.join(" && "); + // await new Promise((resolve) => { + // const child = spawn(DOCKER_BIN, ["image", "pull", "--quiet", spec.image]); + // let timer = setTimeout(() => { + // child.kill("SIGKILL"); + // resolve(new Error("pull timeout")); + // }, 240_000); + + // child.on("close", (code) => { + // clearTimeout(timer); + // resolve(code === 0 ? null : new Error(`pull failed: ${code}`)); + // }); + // }).then((err) => { + // if (err) throw err; + // }) + // } +} + +async function runInContainer({ language, code, args = [], files = [] }: { + language: string, + code: string, + args: string[], + files: fileType[] +}) { + if (!LANGS[language]) throw new Error(`language not allowed: ${language}`); + const spec = LANGS[language]; + + await ensureImage(spec); + + // build the Docker args + const dockerArgs = [ + "run", "--rm", + "--network=none", "--read-only", + "--pids-limit=256", + "--cpus=1", "--memory=512m", + "--cap-drop=ALL", "--security-opt", "no-new-privileges", + "--tmpfs", "/work:rw,exec,size=64m", + "-w", "/work", + "--pull=never", + spec.image + ]; + + // inside the container, write the files and run code + const script = [ + // write the main file using base64 + `echo ${JSON.stringify(Buffer.from(code, "utf8").toString("base64"))} | base64 -d > ${spec.filename}`, + // write any extra files using base64 + ...files.flatMap((f) => [ + `mkdir -p "$(dirname "${f.path}")"`, + `echo ${JSON.stringify(Buffer.from(f.content, "utf8").toString("base64"))} | base64 -d > "${f.path}"`, + ]), + // run it + `${spec.run.join(' ')} ${args.map((a) => JSON.stringify(a)).join(' ')}` + ].join('\n'); + + dockerArgs.push("sh", "-lc", script); + + const result = await new Promise((resolve) => { + const child = spawn(DOCKER_BIN, dockerArgs, { stdio: ["ignore", "pipe", "pipe"] }); + let stdout = "", stderr = ""; + + const timer = setTimeout(() => { + child.kill("SIGKILL"); + resolve({ stdout, stderr: stderr + "\n[killed: timeout]", exitCode: 137, timedOut: true }); + }, 25_000); + + child.stdout.on("data", (d) => { stdout += d.toString(); }); + child.stderr.on("data", (d) => { stderr += d.toString(); }); + child.on("close", (code) => { + clearTimeout(timer); + resolve({ stdout, stderr, exitCode: code ?? 1, timedOut: false }); + }); + }); + + return result; +} + +const server = http.createServer(async (req, res) => { + try { + console.debug(`recieved ${req.method} request on ${req.url}`); + + if (req.method === "GET" && req.url === "/openapi.json") { + sendJson(res, 200, OPENAPI); + return; + } + + if (req.method === "GET" && req.url === "/") { + res.writeHead(200); + res.end("Ok"); + return; + } + + if (req.method === "POST" && req.url === "/execute") { + let body = ""; + req.on("data", (c) => { body += c.toString(); }); + req.on("end", async () => { + try { + const payload = JSON.parse(body || "{}"); + const out = await runInContainer(payload); + sendJson(res, 200, out); + } catch (e) { + sendJson(res, 400, { error: String(e?.message || e) }); + } + }); + return; + } + + res.writeHead(404); + res.end("not found"); + } catch (e) { + sendJson(res, 500, { error: String(e?.message || e) }); + } +}); + +server.listen(PORT, HOST, () => { + console.log(`[runner] listening on http://${HOST}:${PORT}`); +}); + diff --git a/coderunner/package-lock.json b/coderunner/package-lock.json new file mode 100644 index 0000000..ff32074 --- /dev/null +++ b/coderunner/package-lock.json @@ -0,0 +1,29 @@ +{ + "name": "coderunner", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "devDependencies": { + "@types/node": "^24.3.1" + } + }, + "node_modules/@types/node": { + "version": "24.3.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.1.tgz", + "integrity": "sha512-3vXmQDXy+woz+gnrTvuvNrPzekOi+Ds0ReMxw0LzBiK3a+1k0kQn9f2NWk+lgD4rJehFUmYy2gMhJ2ZI+7YP9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.10.0" + } + }, + "node_modules/undici-types": { + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz", + "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/coderunner/package.json b/coderunner/package.json new file mode 100644 index 0000000..1220716 --- /dev/null +++ b/coderunner/package.json @@ -0,0 +1,5 @@ +{ + "devDependencies": { + "@types/node": "^24.3.1" + } +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..9f36c7c --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,119 @@ +services: + open-webui: + image: ghcr.io/open-webui/open-webui:main + container_name: open-webui + ports: + - "4000:8080" + volumes: + - open-webui:/app/backend/data + extra_hosts: + - host.docker.internal:host-gateway + restart: always + depends_on: + - postgres + networks: + - internal + + postgres: + image: postgres:latest + container_name: openwebui_postgres + restart: always + environment: + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=mypassword + - POSTGRES_DB=openwebui_db + volumes: + - pgdata:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres"] + interval: 10s + timeout: 5s + retries: 5 + networks: + - internal + + # 8080 + searxng: + image: searxng/searxng:latest + container_name: searxng + volumes: + - ./searxng.yml:/etc/searxng/settings.yml:ro,Z + - searxng_data:/etc/searxng:rw + restart: always + + # DELETEME: for local testing only (extern port closed) + ports: + - "4001:8080" + networks: + - internal + + coderunner: + build: + context: ./coderunner + dockerfile: Dockerfile + restart: unless-stopped + healthcheck: + test: ["CMD", "curl", "-fsS", "http://127.0.0.1:8787/openapi.json"] + interval: 30s + timeout: 3s + retries: 3 + start_period: 10s + + user: "1000:1000" + group_add: + - "977" + + # death + environment: + DOCKER_HOST: "unix:///var/run/docker.sock" + volumes: + - /var/run/docker.sock:/var/run/docker.sock:Z + # - ./tmp:/tmp + + read_only: true + tmpfs: + - /run:rw,nosuid,nodev + - /tmp:rw,exec,nosuid,nodev,size=64m + + security_opt: + - no-new-privileges:true + - label=disable + networks: + - internal + + browser: + build: + context: ./browser + dockerfile: Dockerfile + container_name: browser + networks: + - internal + # playwright/chromium has larger /dev/shm :D + shm_size: "1gb" + user: "1000:1000" + environment: + WEBUI_IP: "0.0.0.0" + WEBUI_PORT: "7788" + ports: + - "7788:7788" + tmpfs: + - /opt/web-ui/tmp:rw,exec,nosuid,nodev,mode=1777,size=64m + volumes: + - webui_data:/data + # - webui_env:/opt/web-ui/.env + healthcheck: + test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:7788').read()"] + interval: 30s + timeout: 5s + retries: 5 + restart: unless-stopped + +volumes: + open-webui: + pgdata: + searxng_data: + webui_data: + +networks: + internal: + driver: bridge diff --git a/searxng.yml b/searxng.yml new file mode 100644 index 0000000..8542051 --- /dev/null +++ b/searxng.yml @@ -0,0 +1,12 @@ +use_default_settings: true + +search: + formats: + - html + - json + +server: + secret_key: "784dd44a8d67eb0b306e508ad414b4e3ebc9d358f81073ca7cb4c0919204c51c" + public_instance: false + limiter: false +