initial commit (no RAG server)
This commit is contained in:
@@ -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"]
|
||||
@@ -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
|
||||
@@ -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"]
|
||||
|
||||
@@ -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}`);
|
||||
});
|
||||
|
||||
Generated
+29
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.3.1"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
use_default_settings: true
|
||||
|
||||
search:
|
||||
formats:
|
||||
- html
|
||||
- json
|
||||
|
||||
server:
|
||||
secret_key: "784dd44a8d67eb0b306e508ad414b4e3ebc9d358f81073ca7cb4c0919204c51c"
|
||||
public_instance: false
|
||||
limiter: false
|
||||
|
||||
Reference in New Issue
Block a user