Download Dockerfile from Hemifield/Vscode: direct link, hf CLI and curl.
- Browser
- Download file 17.5 kB
-
https://huggingface.co/spaces/Hemifield/Vscode/resolve/main/Dockerfile
- Command line
-
hf download hf://spaces/Hemifield/Vscode/Dockerfile
-
curl -L -o Dockerfile https://huggingface.co/spaces/Hemifield/Vscode/resolve/main/Dockerfile
17.5 kB
| # syntax=docker/dockerfile:1 | |
| FROM node:24-slim AS build | |
| ARG CODEX_WEB_REF=0f71e2c901cc901931c8f43730fb41e899f71293 | |
| ENV NPM_CONFIG_PREFIX=/opt/npm-global \ | |
| NPM_CONFIG_UPDATE_NOTIFIER=false \ | |
| NPM_CONFIG_FUND=false \ | |
| NPM_CONFIG_AUDIT=false \ | |
| PATH=/opt/npm-global/bin:$PATH \ | |
| NODE_ENV=production | |
| RUN mkdir -p /opt/npm-global \ | |
| && apt-get update \ | |
| && apt-get install -y --no-install-recommends \ | |
| ca-certificates curl unzip patch python3 build-essential \ | |
| && rm -rf /var/lib/apt/lists/* \ | |
| && curl -fsSL "https://github.com/0xcaff/codex-web/archive/${CODEX_WEB_REF}.tar.gz" \ | |
| | tar -xz -C /opt \ | |
| && mv /opt/codex-web-${CODEX_WEB_REF} /opt/codex-web | |
| COPY --chmod=0755 <<'PY' /opt/apply-hf-patches.py | |
| #!/usr/bin/env python3 | |
| from pathlib import Path | |
| root = Path("/opt/codex-web") | |
| # server: long-lived IPC websocket | |
| main = (root / "src/server/main.ts").read_text() | |
| old_ws = 'const websocketServer = new WebSocketServer({ noServer: true });' | |
| new_ws = 'const websocketServer = new WebSocketServer({ noServer: true, perMessageDeflate: false });' | |
| if old_ws not in main: | |
| raise SystemExit("patch fail: websocketServer constructor") | |
| main = main.replace(old_ws, new_ws, 1) | |
| old_conn = """ websocketServer.on("connection", (socket) => { | |
| sockets.add(socket); | |
| """ | |
| new_conn = """ websocketServer.on("connection", (socket) => { | |
| sockets.add(socket); | |
| try { | |
| const raw = (socket as unknown as { _socket?: { setKeepAlive?: (on: boolean, ms?: number) => void; setNoDelay?: (on: boolean) => void; setTimeout?: (ms: number) => void } })._socket; | |
| raw?.setKeepAlive?.(true, 15000); | |
| raw?.setNoDelay?.(true); | |
| raw?.setTimeout?.(0); | |
| } catch { | |
| /* ignore */ | |
| } | |
| const pingInterval = setInterval(() => { | |
| if (socket.readyState === WebSocket.OPEN) { | |
| try { socket.ping(); } catch { /* proxy may strip ping frames */ } | |
| try { socket.send(JSON.stringify({ type: "keepalive", t: Date.now() })); } catch { /* ignore */ } | |
| } | |
| }, 12000); | |
| """ | |
| if old_conn not in main: | |
| raise SystemExit("patch fail: connection handler") | |
| main = main.replace(old_conn, new_conn, 1) | |
| old_close = """ socket.on("close", () => { | |
| sockets.delete(socket); | |
| """ | |
| new_close = """ socket.on("close", () => { | |
| clearInterval(pingInterval); | |
| sockets.delete(socket); | |
| """ | |
| if old_close not in main: | |
| raise SystemExit("patch fail: close handler") | |
| main = main.replace(old_close, new_close, 1) | |
| old_msg = """ socket.on("message", (rawData) => { | |
| let message: RendererToMainMessage; | |
| try { | |
| message = JSON.parse(String(rawData)) as RendererToMainMessage; | |
| } catch (error) { | |
| console.error("[ipc-bridge] invalid JSON payload", error); | |
| return; | |
| } | |
| """ | |
| new_msg = """ socket.on("message", (rawData) => { | |
| let message: RendererToMainMessage; | |
| try { | |
| message = JSON.parse(String(rawData)) as RendererToMainMessage; | |
| } catch (error) { | |
| console.error("[ipc-bridge] invalid JSON payload", error); | |
| return; | |
| } | |
| const maybeType = (message as { type?: string }).type; | |
| if (maybeType === "keepalive" || maybeType === "keepalive-ack") { | |
| if (maybeType === "keepalive" && socket.readyState === WebSocket.OPEN) { | |
| try { socket.send(JSON.stringify({ type: "keepalive-ack", t: (message as { t?: number }).t })); } catch { /* ignore */ } | |
| } | |
| return; | |
| } | |
| """ | |
| if old_msg not in main: | |
| raise SystemExit("patch fail: message handler") | |
| main = main.replace(old_msg, new_msg, 1) | |
| old_static = """ await app.register(fastifyStatic, { | |
| root: path.resolve(__dirname, "../../scratch/asar/webview"), | |
| prefix: "/", | |
| }); | |
| """ | |
| new_static = """ await app.register(fastifyStatic, { | |
| root: path.resolve(__dirname, "../../scratch/asar/webview"), | |
| prefix: "/", | |
| maxAge: 86400, | |
| setHeaders: (res, filePath) => { | |
| if (filePath.endsWith("index.html") || filePath.endsWith(".html")) { | |
| res.setHeader("Cache-Control", "no-cache"); | |
| } else if (/\\.[0-9a-f]{8,}\\.(js|css|woff2?|png|svg|webp)$/i.test(filePath)) { | |
| res.setHeader("Cache-Control", "public, max-age=31536000, immutable"); | |
| } else { | |
| res.setHeader("Cache-Control", "public, max-age=86400"); | |
| } | |
| }, | |
| }); | |
| """ | |
| if old_static not in main: | |
| raise SystemExit("patch fail: static register") | |
| main = main.replace(old_static, new_static, 1) | |
| old_listen = """ await app.listen({ host: options.host, port: options.port }); | |
| console.log(`IPC bridge listening at ws://${options.host}:${options.port}`); | |
| """ | |
| new_listen = """ app.server.requestTimeout = 0; | |
| app.server.headersTimeout = 0; | |
| app.server.keepAliveTimeout = 120000; | |
| app.server.timeout = 0; | |
| await app.listen({ host: options.host, port: options.port }); | |
| console.log(`IPC bridge listening at ws://${options.host}:${options.port}`); | |
| """ | |
| if old_listen not in main: | |
| raise SystemExit("patch fail: listen") | |
| main = main.replace(old_listen, new_listen, 1) | |
| # gzip static assets only β never a streaming/IPC path | |
| if 'import fastifyCompress' not in main: | |
| main = main.replace( | |
| 'import fastifyStatic from "@fastify/static";', | |
| 'import fastifyCompress from "@fastify/compress";\nimport fastifyStatic from "@fastify/static";', | |
| 1, | |
| ) | |
| if "fastifyCompress" in main and "app.register(fastifyCompress" not in main: | |
| main = main.replace( | |
| " await app.register(fastifyMultipart, {", | |
| """ await app.register(fastifyCompress, { | |
| global: true, | |
| threshold: 1024, | |
| encodings: ["gzip", "deflate"], | |
| customTypes: /^text\\/|javascript|json|xml|svg|wasm/, | |
| }); | |
| await app.register(fastifyMultipart, {""", | |
| 1, | |
| ) | |
| (root / "src/server/main.ts").write_text(main) | |
| pkg = (root / "package.json").read_text() | |
| if '"@fastify/compress"' not in pkg: | |
| pkg = pkg.replace( | |
| '"@fastify/multipart": "^10.0.0",', | |
| '"@fastify/compress": "^9.2.0",\n "@fastify/multipart": "^10.0.0",', | |
| 1, | |
| ) | |
| (root / "package.json").write_text(pkg) | |
| # browser shim: application keepalive (proxies ignore WS ping frames) | |
| shim = (root / "src/browser/shim.ts").read_text() | |
| shim = shim.replace( | |
| "const RECONNECT_DELAY_MS = 1_000;", | |
| "const RECONNECT_DELAY_MS = 400;", | |
| 1, | |
| ) | |
| old_in = """function handleIncomingMessage(message: MainToRendererMessage): void { | |
| if (message.type === "ipc-main-event") { | |
| """ | |
| new_in = """function handleIncomingMessage(message: MainToRendererMessage): void { | |
| const maybeType = (message as { type?: string }).type; | |
| if (maybeType === "keepalive" || maybeType === "keepalive-ack") { | |
| return; | |
| } | |
| if (message.type === "ipc-main-event") { | |
| """ | |
| if old_in not in shim: | |
| raise SystemExit("patch fail: shim handleIncomingMessage") | |
| shim = shim.replace(old_in, new_in, 1) | |
| old_open = """ socket.addEventListener("open", () => { | |
| flushOutboundQueue(); | |
| }); | |
| """ | |
| new_open = """ socket.addEventListener("open", () => { | |
| flushOutboundQueue(); | |
| try { socket.send(JSON.stringify({ type: "keepalive", t: Date.now() })); } catch { /* ignore */ } | |
| }); | |
| """ | |
| if old_open not in shim: | |
| raise SystemExit("patch fail: shim open") | |
| shim = shim.replace(old_open, new_open, 1) | |
| needle = "ensureSocket();\n\nexport const contextBridge" | |
| if needle not in shim: | |
| raise SystemExit("patch fail: shim ensureSocket trailer") | |
| shim = shim.replace( | |
| needle, | |
| """ensureSocket(); | |
| setInterval(() => { | |
| if (socket && socket.readyState === WebSocket.OPEN) { | |
| try { socket.send(JSON.stringify({ type: "keepalive", t: Date.now() })); } catch { /* ignore */ } | |
| } else { | |
| ensureSocket(); | |
| } | |
| }, 12000); | |
| document.addEventListener("visibilitychange", () => { | |
| if (document.visibilityState === "visible") { | |
| ensureSocket(); | |
| } | |
| }); | |
| window.addEventListener("online", () => ensureSocket()); | |
| window.addEventListener("pageshow", () => ensureSocket()); | |
| export const contextBridge""", | |
| 1, | |
| ) | |
| (root / "src/browser/shim.ts").write_text(shim) | |
| # electron getPath: cache/temp off the bucket, userdata stays persisted | |
| elec = (root / "src/server/electron/index.ts").read_text() | |
| old_gp = """ getPath(name: string): string { | |
| log("app.getPath", [name]); | |
| return process.cwd(); | |
| }, | |
| """ | |
| new_gp = """ getPath(name: string): string { | |
| log("app.getPath", [name]); | |
| const home = process.env.HOME || "/home/user"; | |
| const persist = process.env.CODEX_WEB_USERDATA || home + "/app-data"; | |
| const tmp = process.env.CODEX_WEB_CACHE || "/var/tmp/codex-electron"; | |
| switch (name) { | |
| case "home": | |
| return home; | |
| case "temp": | |
| case "cache": | |
| case "userCache": | |
| case "crashDumps": | |
| return tmp; | |
| case "logs": | |
| return tmp + "/logs"; | |
| case "desktop": | |
| case "documents": | |
| case "downloads": | |
| case "music": | |
| case "pictures": | |
| case "videos": | |
| return home; | |
| default: | |
| return persist; | |
| } | |
| }, | |
| """ | |
| if old_gp not in elec: | |
| raise SystemExit("patch fail: electron getPath") | |
| elec = elec.replace(old_gp, new_gp, 1) | |
| (root / "src/server/electron/index.ts").write_text(elec) | |
| # smaller, faster webview preload bundle | |
| vite = (root / "vite.browser.config.ts").read_text() | |
| vite = vite.replace("minify: false,", "minify: true,", 1) | |
| vite = vite.replace("sourcemap: true,", "sourcemap: false,", 1) | |
| (root / "vite.browser.config.ts").write_text(vite) | |
| print("hf patches applied") | |
| PY | |
| RUN python3 /opt/apply-hf-patches.py \ | |
| && cd /opt/codex-web \ | |
| && npm install --omit=dev \ | |
| && npm install -g @openai/codex \ | |
| && npm cache clean --force \ | |
| && chmod -R a+rX /opt/codex-web /opt/npm-global | |
| FROM node:24-slim | |
| ENV NPM_CONFIG_PREFIX=/opt/npm-global \ | |
| NPM_CONFIG_UPDATE_NOTIFIER=false \ | |
| NPM_CONFIG_FUND=false \ | |
| NPM_CONFIG_AUDIT=false \ | |
| PATH=/opt/npm-global/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin \ | |
| NODE_ENV=production \ | |
| NODE_OPTIONS=--dns-result-order=ipv4first | |
| RUN apt-get update \ | |
| && apt-get install -y --no-install-recommends \ | |
| ca-certificates curl wget git tini sudo gosu \ | |
| python3 python3-pip python3-venv python3-dev \ | |
| gcc g++ make cmake pkg-config \ | |
| unzip zip \ | |
| libssl-dev libffi-dev \ | |
| procps bash \ | |
| ripgrep fd-find \ | |
| && rm -rf /var/lib/apt/lists/* \ | |
| && ln -sf "$(command -v fdfind || true)" /usr/local/bin/fd || true \ | |
| && groupmod -n user node \ | |
| && usermod -l user -d /home/user -m node \ | |
| && usermod -aG sudo user \ | |
| && mkdir -p /home/user/app /data /var/tmp/codex-ephemeral /opt/npm-global \ | |
| && echo "user ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/user \ | |
| && chmod 440 /etc/sudoers.d/user \ | |
| && chown -R user:user /home/user /data /var/tmp/codex-ephemeral | |
| COPY --from=build /opt/codex-web /opt/codex-web | |
| COPY --from=build /opt/npm-global /opt/npm-global | |
| RUN chown -R user:user /opt/npm-global \ | |
| && chmod -R a+rX /opt/codex-web /opt/npm-global | |
| COPY --chmod=0755 <<'EOF' /usr/local/bin/entrypoint | |
| #!/usr/bin/env bash | |
| set -euo pipefail | |
| DATA_DIR="/data" | |
| EPHEM="/var/tmp/codex-ephemeral" | |
| mkdir -p "$EPHEM" /var/tmp/codex-electron /tmp | |
| log() { echo "[entrypoint] $*"; } | |
| # Persist $HOME onto the bucket; never ls/du/rm -rf large FUSE trees on the boot path. | |
| if [ -w "$DATA_DIR" ]; then | |
| log "storage bucket detected at $DATA_DIR β persisting the entire home directory." | |
| PERSIST_HOME="$DATA_DIR/home" | |
| mkdir -p "$PERSIST_HOME" | |
| LISTING="$(timeout 8 ls -A "$PERSIST_HOME" 2>/dev/null || true)" | |
| if [ -z "$LISTING" ]; then | |
| log "bucket looks empty β seeding from the image's baked-in \$HOME" | |
| timeout 60 cp -a /home/user/. "$PERSIST_HOME"/ 2>/dev/null \ | |
| || log "WARNING: seed copy timed out or failed β continuing" | |
| fi | |
| export HOME="$PERSIST_HOME" | |
| else | |
| log "WARNING: no writable storage bucket at $DATA_DIR β EPHEMERAL storage, lost on restart." | |
| log "Attach a bucket at mount path '/data', Access: Read & Write, in Settings -> Storage." | |
| export HOME="${HOME:-/home/user}" | |
| fi | |
| # If started as root: chown the persist tree, bind-mount /usr/local, then re-exec as uid 1000 | |
| # before any Codex files are written so bucket objects are not stuck root-owned. | |
| if [ "$(id -u)" = "0" ] && [ "${1:-}" != "--as-user" ]; then | |
| mkdir -p "$HOME/.usr-local" "$HOME/.local" "$HOME/.codex" "$HOME/app-data" | |
| chown user:user "$HOME" "$HOME/.usr-local" "$HOME/.local" "$HOME/.codex" "$HOME/app-data" 2>/dev/null || true | |
| if [ -z "$(timeout 5 ls -A "$HOME/.usr-local" 2>/dev/null || true)" ]; then | |
| timeout 30 cp -a /usr/local/. "$HOME/.usr-local"/ 2>/dev/null || true | |
| chown -R user:user "$HOME/.usr-local" 2>/dev/null || true | |
| fi | |
| if mount --bind "$HOME/.usr-local" /usr/local 2>/dev/null; then | |
| log "bind-mounted \$HOME/.usr-local -> /usr/local (compilers you install here persist)" | |
| else | |
| log "could not bind-mount /usr/local (no CAP_SYS_ADMIN) β use \$HOME/.local instead" | |
| fi | |
| log "dropping privileges to user uid 1000" | |
| exec gosu user "$0" --as-user | |
| fi | |
| export CODEX_HOME="$HOME/.codex" | |
| mkdir -p "$CODEX_HOME" "$HOME/.local/bin" "$HOME/.local/lib" "$HOME/app-data" "$HOME/.usr-local" | |
| if ! timeout 15 touch "$HOME/.write-probe" 2>/dev/null; then | |
| log "FATAL: $HOME is not writable by uid $(id -u) (or the bucket didn't respond within 15s)." | |
| log "Check Settings -> Storage: Access mode must be Read & Write, then do a full Restart." | |
| exit 1 | |
| fi | |
| rm -f "$HOME/.write-probe" || true | |
| # Divert only Codex runtime bloat onto fast local disk; the rest of $HOME stays on the bucket. | |
| # Symlink, not env-var: Codex hardcodes ~/.cache/codex-runtimes and ignores XDG_CACHE_HOME. | |
| divert_to_ephemeral() { | |
| local src="$1" | |
| local dest="$2" | |
| mkdir -p "$(dirname "$src")" "$dest" | |
| if [ -L "$src" ]; then | |
| ln -sfn "$dest" "$src" | |
| return 0 | |
| fi | |
| if [ -e "$src" ]; then | |
| local stale="${src}.STALE.$$" | |
| if mv "$src" "$stale" 2>/dev/null; then | |
| log "moved existing $(basename "$src") off the live path β deleting in background" | |
| ( nice rm -rf "$stale" >/dev/null 2>&1 || true ) & | |
| else | |
| log "WARNING: could not rename $src β leaving it and still linking" | |
| fi | |
| fi | |
| ln -sfn "$dest" "$src" | |
| } | |
| mkdir -p "$HOME/.cache" "$HOME/.local/state/codex" | |
| divert_to_ephemeral "$HOME/.cache/codex-runtimes" "$EPHEM/codex-runtimes" | |
| divert_to_ephemeral "$HOME/.cache/codex-primary-runtime" "$EPHEM/codex-primary-runtime" | |
| divert_to_ephemeral "$HOME/.local/state/codex/logs" "$EPHEM/codex-logs-xdg" | |
| if compgen -G "$HOME/.cache/codex-runtimes.STALE*" >/dev/null \ | |
| || compgen -G "$HOME/.cache/codex-primary-runtime.STALE*" >/dev/null; then | |
| log "sweeping leftover STALE runtime dirs in the background" | |
| ( nice rm -rf "$HOME/.cache"/codex-runtimes.STALE* \ | |
| "$HOME/.cache"/codex-primary-runtime.STALE* >/dev/null 2>&1 || true ) & | |
| fi | |
| # Do NOT export XDG_CACHE_HOME / XDG_STATE_HOME to /tmp β that would throw | |
| # non-Codex caches (pip, huggingface, etc.) off the bucket too. | |
| export CODEX_WEB_USERDATA="$HOME/app-data" | |
| export CODEX_WEB_CACHE="/var/tmp/codex-electron" | |
| export PYTHONUSERBASE="$HOME/.local" | |
| export PIP_USER=1 | |
| export npm_config_prefix="$HOME/.local" | |
| export NPM_CONFIG_PREFIX="$HOME/.local" | |
| export CARGO_HOME="$HOME/.cargo" | |
| export RUSTUP_HOME="$HOME/.rustup" | |
| export GOPATH="$HOME/go" | |
| export PATH="$HOME/.local/bin:$HOME/.cargo/bin:$HOME/go/bin:/opt/npm-global/bin:$PATH" | |
| mkdir -p "$HOME/.local/bin" "$CARGO_HOME/bin" "$GOPATH/bin" "$CODEX_WEB_USERDATA" | |
| # Codex CLI: image ships a working binary; only install if missing. | |
| if ! command -v codex >/dev/null 2>&1; then | |
| log "codex missing from PATH β installing latest into \$HOME/.local ..." | |
| npm install -g @openai/codex \ | |
| || { log "FATAL: could not install @openai/codex"; exit 1; } | |
| fi | |
| export CODEX_CLI_PATH="$(command -v codex)" | |
| if [ -z "${CODEX_CLI_PATH}" ]; then | |
| log "FATAL: 'codex' isn't on PATH." | |
| exit 1 | |
| fi | |
| log "codex: $($CODEX_CLI_PATH --version 2>/dev/null || echo unknown) at $CODEX_CLI_PATH" | |
| if [ -n "${OPENAI_API_KEY:-}" ] && [ ! -f "$CODEX_HOME/auth.json" ]; then | |
| log "logging in to Codex from \$OPENAI_API_KEY ..." | |
| printenv OPENAI_API_KEY | codex login --with-api-key \ | |
| || log "WARNING: codex login failed β check that OPENAI_API_KEY is valid." | |
| elif [ -z "${OPENAI_API_KEY:-}" ] && [ ! -f "$CODEX_HOME/auth.json" ]; then | |
| log "WARNING: no OPENAI_API_KEY and no existing login β Codex won't be usable until set." | |
| fi | |
| ( | |
| set +e | |
| sleep 8 | |
| LATEST="$(timeout 40 npm view @openai/codex version 2>/dev/null || true)" | |
| CURRENT="$(codex --version 2>/dev/null | awk '{print $NF}' || true)" | |
| if [ -n "$LATEST" ] && [ -n "$CURRENT" ] && [ "$LATEST" != "$CURRENT" ]; then | |
| log "codex: $CURRENT -> $LATEST (background, into \$HOME/.local)" | |
| npm install -g "@openai/codex@${LATEST}" \ | |
| && log "codex: now $(codex --version 2>/dev/null)" | |
| fi | |
| ) & | |
| ( | |
| set +e | |
| SEL="$HOME/.dpkg-selections" | |
| if [ -f "$SEL" ] && command -v sudo >/dev/null 2>&1; then | |
| log "replaying persisted apt selections in the background" | |
| timeout 90 sudo apt-get update -qq | |
| timeout 300 sudo dpkg --set-selections < "$SEL" | |
| timeout 600 sudo apt-get dselect-upgrade -y -qq | |
| fi | |
| while true; do | |
| if command -v dpkg >/dev/null 2>&1; then | |
| dpkg --get-selections > "$SEL.tmp" 2>/dev/null && mv "$SEL.tmp" "$SEL" | |
| fi | |
| sleep 180 | |
| done | |
| ) & | |
| cd "$CODEX_WEB_USERDATA" | |
| log "serving on 0.0.0.0:7860 β no auth in front of this (codex-web has none by design)" | |
| exec node /opt/codex-web/src/server/main.js --host 0.0.0.0 --port 7860 | |
| EOF | |
| ENV HOME=/home/user | |
| WORKDIR /home/user/app | |
| EXPOSE 7860 | |
| ENTRYPOINT ["/usr/bin/tini", "--", "/usr/local/bin/entrypoint"] |