cloudron-pagelm (sha256:7e904880f0c69bcb9a57ebaaaa5ba51e352ed690dd916d16129451d506148ca9)

Published 2026-08-19 08:23:37 +00:00 by timconsidine in cca/cloudron-pagelm

Installation

docker pull forgejo.tcjc.uk/cca/cloudron-pagelm@sha256:7e904880f0c69bcb9a57ebaaaa5ba51e352ed690dd916d16129451d506148ca9
sha256:7e904880f0c69bcb9a57ebaaaa5ba51e352ed690dd916d16129451d506148ca9

Image layers

ARG RELEASE
ARG LAUNCHPAD_BUILD_ARCH
LABEL org.opencontainers.image.version=24.04
ADD file:d938ff3d4eee15d8600de84bf85eac6ecd0f20bc92dfe305dafbff0bdc974c0f in /
CMD ["/bin/bash"]
ENV DEBIAN_FRONTEND=noninteractive
RUN /bin/sh -c echo 'Acquire::http {No-Cache=True;};' > /etc/apt/apt.conf.d/no-cache && echo 'APT::Install-Recommends "0"; APT::Install-Suggests "0";' > /etc/apt/apt.conf.d/01norecommend && echo 'Dir::Cache { srcpkgcache ""; pkgcache ""; }' > /etc/apt/apt.conf.d/02nocache && echo 'Acquire::GzipIndexes "true"; Acquire::CompressionTypes::Order:: "gz";' > /etc/apt/apt.conf.d/02compress-indexes # buildkit
SHELL [/bin/bash -c]
RUN /bin/bash -c apt -y update && apt -y --no-install-recommends install ca-certificates curl dirmngr git gpg gpg-agent wget unzip zip software-properties-common build-essential make gcc g++ sudo cron dos2unix tini attr && curl -fsSL https://packages.redis.io/gpg | sudo gpg --dearmor -o /usr/share/keyrings/redis-archive-keyring.gpg && echo "deb [signed-by=/usr/share/keyrings/redis-archive-keyring.gpg] https://packages.redis.io/deb $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/redis.list && apt -y update && apt -y --no-install-recommends install supervisor dumb-init iputils-ping telnet netcat-openbsd lsof net-tools openssl dnsutils rsync bind9-host stress pv fio iotop nethogs crudini xmlstarlet moreutils jq pwgen swaks vim nano cmake pkg-config openssh-client uuid less zip file apache2 libapache2-mod-perl2 apache2-dev nginx-full optipng pngquant jpegoptim sqlite3 mysql-client-8.0 redis-tools postgresql-client-16 ldap-utils gettext imagemagick graphicsmagick libcurl4 libcurl4-openssl-dev libexpat1-dev libffi-dev libgdbm-dev libicu-dev libmysqlclient-dev libncurses5-dev libpq-dev libre2-dev libreadline-dev libssl-dev libxml2-dev libxslt-dev libyaml-dev zlib1g-dev libmcrypt-dev libgmp-dev libfreetype6-dev libjpeg-dev libjpeg-turbo8-dev libpng-dev chrpath libxft-dev libfontconfig1-dev libkrb5-dev libxslt1-dev libldap2-dev libsasl2-dev libtool libvips libzmq3-dev locales-all locales libmagic1 xmlsec1 perl libimage-exiftool-perl python3-dev python3-pip python3-setuptools python3-venv ghostscript libgs-dev ffmpeg x264 x265 && rm -rf /var/cache/apt /var/lib/apt/lists # buildkit
ARG NODE_VERSION=24.19.0
RUN |1 NODE_VERSION=24.19.0 /bin/bash -c mkdir -p /usr/local/node-$NODE_VERSION && curl -L https://nodejs.org/dist/v$NODE_VERSION/node-v$NODE_VERSION-linux-x64.tar.gz | tar zxf - --strip-components 1 -C /usr/local/node-$NODE_VERSION # buildkit
RUN |1 NODE_VERSION=24.19.0 /bin/bash -c curl https://downloads.mongodb.com/compass/mongodb-mongosh_2.9.2_amd64.deb -o /tmp/mongosh.deb && dpkg -i /tmp/mongosh.deb && rm /tmp/mongosh.deb && curl https://fastdl.mongodb.org/tools/db/mongodb-database-tools-ubuntu2204-x86_64-100.11.0.deb -o /tmp/mongotools.deb && dpkg -i /tmp/mongotools.deb && rm /tmp/mongotools.deb # buildkit
RUN |1 NODE_VERSION=24.19.0 /bin/bash -c curl -L https://github.com/tianon/gosu/releases/download/1.19/gosu-amd64 -o /usr/local/bin/gosu && chmod +x /usr/local/bin/gosu # buildkit
ARG YQVERSION=4.53.3
RUN |2 NODE_VERSION=24.19.0 YQVERSION=4.53.3 /bin/bash -c curl -sL https://github.com/mikefarah/yq/releases/download/v${YQVERSION}/yq_linux_amd64 -o /usr/bin/yq && chmod +x /usr/bin/yq # buildkit
RUN |2 NODE_VERSION=24.19.0 YQVERSION=4.53.3 /bin/bash -c userdel --remove ubuntu && adduser --uid 1000 --disabled-login --gecos 'Cloudron' cloudron && passwd -d cloudron # buildkit
RUN |2 NODE_VERSION=24.19.0 YQVERSION=4.53.3 /bin/bash -c addgroup --gid 500 --system media && usermod -a -G media cloudron && usermod -a -G media www-data # buildkit
RUN |2 NODE_VERSION=24.19.0 YQVERSION=4.53.3 /bin/bash -c echo "set noswapfile" >> /root/.vimrc && gosu cloudron:cloudron bash -c 'echo "set noswapfile" >> /home/cloudron/.vimrc' && echo "unset historylog" >> /etc/nanorc # buildkit
RUN |2 NODE_VERSION=24.19.0 YQVERSION=4.53.3 /bin/bash -c update-locale LANG=en_US.UTF-8 LC_CTYPE=en_US.UTF-8 LC_ALL=en_US.UTF-8 # buildkit
RUN |2 NODE_VERSION=24.19.0 YQVERSION=4.53.3 /bin/bash -c for dir in .cache .config .node-gyp .npm .mongodb .ssh .yarn .local; do rm -rf /root/${dir} && ln -sf /run/root${dir} /root/${dir} && mkdir /run/root${dir}; rm -rf /home/cloudron/${dir} && ln -sf /run/cloudron${dir} /home/cloudron/${dir} && mkdir /run/cloudron${dir} && chown --no-dereference cloudron:cloudron /home/cloudron/${dir}; chown cloudron:cloudron /run/cloudron${dir}; done # buildkit
RUN |2 NODE_VERSION=24.19.0 YQVERSION=4.53.3 /bin/bash -c for file in .bash_history .dbshell .inputrc .irb_history .mongorc.js .mysql_history .psql_history .mongoshrc.js .yarnrc; do ln -sf /run/root${file} /root/${file}; ln -sf /run/cloudron${file} /home/cloudron/${file} && chown --no-dereference cloudron:cloudron /home/cloudron/${file}; done # buildkit
COPY cloudron_addons /usr/local/cloudron_addons # buildkit
RUN |2 NODE_VERSION=24.19.0 YQVERSION=4.53.3 /bin/bash -c echo -e "\nsource /usr/local/cloudron_addons\n\n[[ -f /app/data/.bashrc ]] && source /app/data/.bashrc\n" >> /root/.bashrc # buildkit
ENV PATH=/usr/local/node-24.19.0/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
RUN /bin/bash -c mkdir -p /app/code /app/data # buildkit
RUN /bin/bash -c apt-get update && apt-get install -y --no-install-recommends curl ca-certificates ne ffmpeg build-essential python3 pkg-config && update-ca-certificates --fresh || true && rm -rf /var/lib/apt/lists/* # buildkit
WORKDIR /app/code
RUN /bin/bash -c ln -sf /app/data/node_modules /app/code/node_modules && ln -sf /app/data/storage /app/code/storage && ln -sf /app/data/.env /app/code/.env # buildkit
ARG PAGELM_REF=main
ARG PAGELM_REPO=https://github.com/CaviraOSS/PageLM/archive
RUN |2 PAGELM_REF=main PAGELM_REPO=https://github.com/CaviraOSS/PageLM/archive /bin/bash -c set -eu; TARBALL_URL="${PAGELM_REPO}/${PAGELM_REF}.tar.gz"; echo "==> Downloading PageLM source: ${TARBALL_URL}"; curl -fsSL --retry 3 -o /tmp/pagelm.tar.gz "${TARBALL_URL}"; tar -xzf /tmp/pagelm.tar.gz -C /tmp; SRCDIR=$(find /tmp -maxdepth 1 -type d -name 'PageLM-*' | head -n 1); echo "==> Extracted source to: ${SRCDIR}"; cp -a "${SRCDIR}/backend" /app/code/backend; cp -a "${SRCDIR}/frontend" /app/code/frontend; cp "${SRCDIR}/package.json" /app/code/package.json; cp "${SRCDIR}/package-lock.json" /app/code/package-lock.json || true; cp "${SRCDIR}/.npmrc" /app/code/.npmrc || true; cp "${SRCDIR}/.env.example" /app/code/defaults.env.example; { echo "legacy-peer-deps=true"; } > /app/code/.npmrc-local && if [[ -f /app/code/.npmrc ]]; then cat /app/code/.npmrc-local >> /app/code/.npmrc; else mv /app/code/.npmrc-local /app/code/.npmrc; fi; rm -f /app/code/.npmrc-local; rm -rf "/tmp/pagelm.tar.gz" "${SRCDIR}" # buildkit
RUN |2 PAGELM_REF=main PAGELM_REPO=https://github.com/CaviraOSS/PageLM/archive /bin/bash -c echo "==> Patching backend models (util wrapChat + openai embeddings apiKey + handleAsk string return) + frontend/api.ts same-origin WS" && python3 - <<'PYEOF' import pathlib # 1. FRONTEND: same-origin WS + podcast URLs when env.backend is empty p = pathlib.Path("/app/code/frontend/src/lib/api.ts") t = p.read_text() old_ws = 'function wsURL(path: string) {\n const u = new URL(env.backend);\n const proto = u.protocol === "https:" ? "wss:" : "ws:";\n return `${proto}//${u.host}${path}`;\n}' new_ws = 'function wsURL(path: string) {\n const base = (env.backend && env.backend.length > 0)\n ? env.backend\n : (typeof window !== "undefined" ? window.location.origin : "http://localhost");\n const u = new URL(base);\n const proto = u.protocol === "https:" ? "wss:" : "ws:";\n return `${proto}//${u.host}${path}`;\n}' assert old_ws in t, "could not locate frontend wsURL() block to patch" t = t.replace(old_ws, new_ws) old_pod = 'const wsUrl = `${env.backend.replace(/^http/, "ws")}/ws/podcast?pid=${pid}`' new_pod = 'const _podBase = (env.backend && env.backend.length > 0) ? env.backend : (typeof window !== "undefined" ? window.location.origin : "http://localhost");\n const wsUrl = `${_podBase.replace(/^http/, "ws")}/ws/podcast?pid=${pid}`' assert old_pod in t, "could not locate frontend podcast wsUrl line to patch" t = t.replace(old_pod, new_pod) p.write_text(t) # 2. BACKEND: util.ts wrapChat -> Proxy-based wrapper preserving all LangChain behavior p = pathlib.Path("/app/code/backend/src/utils/llm/models/util.ts") t = p.read_text() old_util = """import type { Msg } from './types' export const wrapChat = (m: any) => ({ invoke: (ms: Msg[]) => m.invoke(ms), call: (ms: Msg[]) => m.invoke(ms), })""" new_util = """import type { Msg } from './types' export const wrapChat = (m: any) => new Proxy(Object(m), { get: (target: any, prop: PropertyKey, receiver: any) => { if (prop === "call") return (ms: Msg[]) => m.invoke(ms); const v = Reflect.get(m, prop, m); if (typeof v === "function") return v.bind(m); return v; }, has: (_: any, prop: PropertyKey) => prop in m, ownKeys: () => Reflect.ownKeys(Object(m)), getOwnPropertyDescriptor: (tgt: any, prop: PropertyKey) => { const d = Reflect.getOwnPropertyDescriptor(Object(m), prop); if (d) return d; try { return Reflect.getOwnPropertyDescriptor(m, prop); } catch { return undefined; } }, getPrototypeOf: () => Object.getPrototypeOf(m), }) as any""" assert old_util in t, "could not locate backend util.ts wrapChat function to patch" t = t.replace(old_util, new_util) p.write_text(t) # 3. BACKEND: openai.ts makeEmbeddings # (a) use cfg.openai_embed (not cfg.openai) as apiKey # (b) add OPENAI_EMBED_FULL_URL support — path-style / model-in-URL providers like # omniroute require posting to a pre-built full URL without the SDK appending # "/embeddings". When OPENAI_EMBED_FULL_URL is set we return a thin native # fetch() wrapper instead of @langchain/openai OpenAIEmbeddings class. p = pathlib.Path("/app/code/backend/src/utils/llm/models/openai.ts") t = p.read_text() old_emb_api = " apiKey: cfg.openai || process.env.OPENAI_EMBED_API_KEY," new_emb_api = " apiKey: cfg.openai_embed || process.env.OPENAI_EMBED_API_KEY," assert old_emb_api in t, "could not locate openai.ts makeEmbeddings apiKey bug line to patch" t = t.replace(old_emb_api, new_emb_api) old_make_emb = """export const makeEmbeddings: MkEmb = (cfg: any): EmbeddingsLike => { return new OpenAIEmbeddings({ model: cfg.openai_embed_model || 'text-embedding-3-large', apiKey: cfg.openai_embed || process.env.OPENAI_EMBED_API_KEY, configuration: { baseURL: process.env.OPENAI_EMBED_BASE_URL, } }) }""" new_make_emb = """class _CloudronOpenAIEmbeddingsNative { private readonly url: string; private readonly apiKey: string; private readonly model: string; constructor(opts: any) { this.url = opts.url; this.apiKey = opts.apiKey; this.model = opts.model; } async _run(inputs: string[]): Promise<number[][]> { const body = { model: this.model, input: inputs, encoding_format: "float" }; const res = await fetch(this.url, { method: "POST", headers: { "Authorization": `Bearer ${this.apiKey}`, "Content-Type": "application/json", "Accept": "application/json", "User-Agent": "pagelm-cloudron-native-emb/1.0", }, body: JSON.stringify(body), }); const txt = await res.text(); let data: any; try { data = JSON.parse(txt); } catch (e) { throw new Error(`Embeddings HTTP ${res.status} invalid JSON: ${txt.slice(0,400)}`); } if (!res.ok || data?.error) { const msg = data?.error?.message || data?.message || txt; throw new Error(`Embeddings HTTP ${res.status}: ${String(msg).slice(0,400)}`); } if (!data || !Array.isArray(data.data) || data.data.length === 0) { throw new Error(`Embeddings missing data[] array in response: ${txt.slice(0,400)}`); } const out: number[][] = new Array(data.data.length); for (let i = 0; i < data.data.length; i++) { const d = data.data[i]; const e = d && (d.embedding ?? d.vector ?? d.values ?? d.value); if (!e || !Array.isArray(e)) throw new Error(`Embeddings item ${i} missing array. ${txt.slice(0,200)}`); out[d.index ?? i] = e.map(Number); } return out; } async embedDocuments(texts: string[]): Promise<number[][]> { return this._run(texts); } async embedQuery(text: string): Promise<number[]> { const r = await this._run([text]); return r[0]; } } export const makeEmbeddings: MkEmb = (cfg: any): EmbeddingsLike => { const fullUrl: string | undefined = process.env.OPENAI_EMBED_FULL_URL || undefined; if (fullUrl && fullUrl.length > 0 && /^https?:\\/\\//i.test(fullUrl)) { return new _CloudronOpenAIEmbeddingsNative({ url: fullUrl, apiKey: (cfg.openai_embed || process.env.OPENAI_EMBED_API_KEY || "") as string, model: (cfg.openai_embed_model || process.env.OPENAI_EMBED_MODEL || "text-embedding-3-large") as string, }); } return new OpenAIEmbeddings({ model: cfg.openai_embed_model || 'text-embedding-3-large', apiKey: cfg.openai_embed || process.env.OPENAI_EMBED_API_KEY, configuration: { baseURL: process.env.OPENAI_EMBED_BASE_URL, } }) }""" assert old_make_emb in t, "could not locate makeEmbeddings function body in openai.ts" t = t.replace(old_make_emb, new_make_emb) p.write_text(t) # 4. BACKEND: core/routes/chat.ts — handleAsk returns AskPayload { topic, answer, flashcards } # Upstream handleAsk returns a structured object (correct for planner services which # access result.answer / result.flashcards directly). But the chat route stores and # emits the raw object as `content`/WS answer payload → stringified as "[object Object]". # FIX: immediately after handleAsk() in chat.ts, unwrap to markdown string + append # flashcards as GFM <details> section. Planner services are untouched (still receive # the structured object via direct handleAsk() imports). p = pathlib.Path("/app/code/backend/src/core/routes/chat.ts") t = p.read_text() old_chat = """ answer = await handleAsk({ q, namespace: ns, history: relevantHistory, }); await addMsg(id, { role: "assistant", content: answer, at: Date.now(), }); emitToAll(chatSockets.get(id), { type: "answer", answer });""" new_chat = """ const askResult: any = await handleAsk({ q, namespace: ns, history: relevantHistory, }); if (typeof askResult === "string") { answer = askResult } else { const ans: string = typeof askResult?.answer === "string" ? askResult.answer : "" const cards: any[] = Array.isArray(askResult?.flashcards) ? askResult.flashcards : [] let append = "" if (cards.length) { append = "\\n\\n<details><summary>📇 Flashcards (" + cards.length + ")</summary>\\n\\n" for (let ci = 0; ci < cards.length; ci++) { const c = cards[ci] || {} const qStr = typeof c.q === "string" ? c.q.replace(/\\n/g, " ") : "" const aStr = typeof c.a === "string" ? c.a.replace(/\\n/g, "\\n> ") : "" append += "**" + (ci + 1) + ".** " + qStr + "\\n\\n" append += "> " + aStr + "\\n\\n" if (Array.isArray(c.tags) && c.tags.length) { append += "_tags: " + c.tags.map((tc: any) => String(tc)).join(", ") + "_\\n\\n" } } append += "</details>\\n" } answer = ans ? (ans + append) : (JSON.stringify(askResult) + append) } await addMsg(id, { role: "assistant", content: answer, at: Date.now(), }); emitToAll(chatSockets.get(id), { type: "answer", answer })""" assert old_chat in t, "could not locate chat.ts handleAsk → addMsg/emit block for string-unwrap patch" t = t.replace(old_chat, new_chat) p.write_text(t) PYEOF # buildkit
RUN |2 PAGELM_REF=main PAGELM_REPO=https://github.com/CaviraOSS/PageLM/archive /bin/bash -c echo "==> Installing root/backend dependencies" && npm install --legacy-peer-deps --no-audit --no-fund # buildkit
RUN |2 PAGELM_REF=main PAGELM_REPO=https://github.com/CaviraOSS/PageLM/archive /bin/bash -c echo "==> Building backend (TypeScript)" && npm run build # buildkit
RUN |2 PAGELM_REF=main PAGELM_REPO=https://github.com/CaviraOSS/PageLM/archive /bin/bash -c echo "==> Installing frontend dependencies" && cd /app/code/frontend && if [[ -f .npmrc ]]; then echo "legacy-peer-deps=true" >> .npmrc; else echo "legacy-peer-deps=true" > .npmrc; fi && npm install --legacy-peer-deps --no-audit --no-fund # buildkit
RUN |2 PAGELM_REF=main PAGELM_REPO=https://github.com/CaviraOSS/PageLM/archive /bin/bash -c echo "==> Building frontend (Vite) with same-origin API" && cd /app/code/frontend && VITE_BACKEND_URL="" VITE_TIMEOUT=90000 npm run build && cp -a /app/code/frontend/dist /app/code/frontend-dist && rm -rf /app/code/frontend/node_modules /app/code/frontend/dist # buildkit
RUN |2 PAGELM_REF=main PAGELM_REPO=https://github.com/CaviraOSS/PageLM/archive /bin/bash -c mkdir -p /app/code/defaults && cp /app/code/defaults.env.example /app/code/defaults/.env.example && rm -f /app/code/defaults.env.example # buildkit
COPY cloudron-entry.js /app/code/cloudron-entry.js # buildkit
COPY start.sh /app/code/start.sh # buildkit
RUN |2 PAGELM_REF=main PAGELM_REPO=https://github.com/CaviraOSS/PageLM/archive /bin/bash -c chmod +x /app/code/start.sh # buildkit
CMD ["/app/code/start.sh"]

Labels

Key Value
org.opencontainers.image.version 24.04
Details
Container
2026-08-19 08:23:37 +00:00
1
OCI / Docker
linux/amd64
1.1 GiB
Versions (5) View all
0.0.9 2026-08-19
0.0.8 2026-08-19
0.0.7 2026-08-18
0.0.6 2026-08-18
0.0.5 2026-08-18