Hemifield commited on
Commit
bed91b8
Β·
verified Β·
1 Parent(s): c223f2b

Create Dockerfile

Browse files
Files changed (1) hide show
  1. Dockerfile +492 -0
Dockerfile ADDED
@@ -0,0 +1,492 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # syntax=docker/dockerfile:1
2
+
3
+ FROM node:24-slim AS build
4
+
5
+ ARG CODEX_WEB_REF=0f71e2c901cc901931c8f43730fb41e899f71293
6
+
7
+ ENV NPM_CONFIG_PREFIX=/opt/npm-global \
8
+ NPM_CONFIG_UPDATE_NOTIFIER=false \
9
+ NPM_CONFIG_FUND=false \
10
+ NPM_CONFIG_AUDIT=false \
11
+ PATH=/opt/npm-global/bin:$PATH \
12
+ NODE_ENV=production
13
+
14
+ RUN mkdir -p /opt/npm-global \
15
+ && apt-get update \
16
+ && apt-get install -y --no-install-recommends \
17
+ ca-certificates curl unzip patch python3 build-essential \
18
+ && rm -rf /var/lib/apt/lists/* \
19
+ && curl -fsSL "https://github.com/0xcaff/codex-web/archive/${CODEX_WEB_REF}.tar.gz" \
20
+ | tar -xz -C /opt \
21
+ && mv /opt/codex-web-${CODEX_WEB_REF} /opt/codex-web
22
+
23
+ COPY --chmod=0755 <<'PY' /opt/apply-hf-patches.py
24
+ #!/usr/bin/env python3
25
+ from pathlib import Path
26
+
27
+ root = Path("/opt/codex-web")
28
+
29
+ # server: long-lived IPC websocket
30
+ main = (root / "src/server/main.ts").read_text()
31
+
32
+ old_ws = 'const websocketServer = new WebSocketServer({ noServer: true });'
33
+ new_ws = 'const websocketServer = new WebSocketServer({ noServer: true, perMessageDeflate: false });'
34
+ if old_ws not in main:
35
+ raise SystemExit("patch fail: websocketServer constructor")
36
+ main = main.replace(old_ws, new_ws, 1)
37
+
38
+ old_conn = """ websocketServer.on("connection", (socket) => {
39
+ sockets.add(socket);
40
+ """
41
+ new_conn = """ websocketServer.on("connection", (socket) => {
42
+ sockets.add(socket);
43
+ try {
44
+ const raw = (socket as unknown as { _socket?: { setKeepAlive?: (on: boolean, ms?: number) => void; setNoDelay?: (on: boolean) => void; setTimeout?: (ms: number) => void } })._socket;
45
+ raw?.setKeepAlive?.(true, 15000);
46
+ raw?.setNoDelay?.(true);
47
+ raw?.setTimeout?.(0);
48
+ } catch {
49
+ /* ignore */
50
+ }
51
+ const pingInterval = setInterval(() => {
52
+ if (socket.readyState === WebSocket.OPEN) {
53
+ try { socket.ping(); } catch { /* proxy may strip ping frames */ }
54
+ try { socket.send(JSON.stringify({ type: "keepalive", t: Date.now() })); } catch { /* ignore */ }
55
+ }
56
+ }, 12000);
57
+ """
58
+ if old_conn not in main:
59
+ raise SystemExit("patch fail: connection handler")
60
+ main = main.replace(old_conn, new_conn, 1)
61
+
62
+ old_close = """ socket.on("close", () => {
63
+ sockets.delete(socket);
64
+ """
65
+ new_close = """ socket.on("close", () => {
66
+ clearInterval(pingInterval);
67
+ sockets.delete(socket);
68
+ """
69
+ if old_close not in main:
70
+ raise SystemExit("patch fail: close handler")
71
+ main = main.replace(old_close, new_close, 1)
72
+
73
+ old_msg = """ socket.on("message", (rawData) => {
74
+ let message: RendererToMainMessage;
75
+ try {
76
+ message = JSON.parse(String(rawData)) as RendererToMainMessage;
77
+ } catch (error) {
78
+ console.error("[ipc-bridge] invalid JSON payload", error);
79
+ return;
80
+ }
81
+ """
82
+ new_msg = """ socket.on("message", (rawData) => {
83
+ let message: RendererToMainMessage;
84
+ try {
85
+ message = JSON.parse(String(rawData)) as RendererToMainMessage;
86
+ } catch (error) {
87
+ console.error("[ipc-bridge] invalid JSON payload", error);
88
+ return;
89
+ }
90
+
91
+ const maybeType = (message as { type?: string }).type;
92
+ if (maybeType === "keepalive" || maybeType === "keepalive-ack") {
93
+ if (maybeType === "keepalive" && socket.readyState === WebSocket.OPEN) {
94
+ try { socket.send(JSON.stringify({ type: "keepalive-ack", t: (message as { t?: number }).t })); } catch { /* ignore */ }
95
+ }
96
+ return;
97
+ }
98
+ """
99
+ if old_msg not in main:
100
+ raise SystemExit("patch fail: message handler")
101
+ main = main.replace(old_msg, new_msg, 1)
102
+
103
+ old_static = """ await app.register(fastifyStatic, {
104
+ root: path.resolve(__dirname, "../../scratch/asar/webview"),
105
+ prefix: "/",
106
+ });
107
+ """
108
+ new_static = """ await app.register(fastifyStatic, {
109
+ root: path.resolve(__dirname, "../../scratch/asar/webview"),
110
+ prefix: "/",
111
+ maxAge: 86400,
112
+ setHeaders: (res, filePath) => {
113
+ if (filePath.endsWith("index.html") || filePath.endsWith(".html")) {
114
+ res.setHeader("Cache-Control", "no-cache");
115
+ } else if (/\\.[0-9a-f]{8,}\\.(js|css|woff2?|png|svg|webp)$/i.test(filePath)) {
116
+ res.setHeader("Cache-Control", "public, max-age=31536000, immutable");
117
+ } else {
118
+ res.setHeader("Cache-Control", "public, max-age=86400");
119
+ }
120
+ },
121
+ });
122
+ """
123
+ if old_static not in main:
124
+ raise SystemExit("patch fail: static register")
125
+ main = main.replace(old_static, new_static, 1)
126
+
127
+ old_listen = """ await app.listen({ host: options.host, port: options.port });
128
+ console.log(`IPC bridge listening at ws://${options.host}:${options.port}`);
129
+ """
130
+ new_listen = """ app.server.requestTimeout = 0;
131
+ app.server.headersTimeout = 0;
132
+ app.server.keepAliveTimeout = 120000;
133
+ app.server.timeout = 0;
134
+ await app.listen({ host: options.host, port: options.port });
135
+ console.log(`IPC bridge listening at ws://${options.host}:${options.port}`);
136
+ """
137
+ if old_listen not in main:
138
+ raise SystemExit("patch fail: listen")
139
+ main = main.replace(old_listen, new_listen, 1)
140
+
141
+ # gzip static assets only β€” never a streaming/IPC path
142
+ if 'import fastifyCompress' not in main:
143
+ main = main.replace(
144
+ 'import fastifyStatic from "@fastify/static";',
145
+ 'import fastifyCompress from "@fastify/compress";\nimport fastifyStatic from "@fastify/static";',
146
+ 1,
147
+ )
148
+ if "fastifyCompress" in main and "app.register(fastifyCompress" not in main:
149
+ main = main.replace(
150
+ " await app.register(fastifyMultipart, {",
151
+ """ await app.register(fastifyCompress, {
152
+ global: true,
153
+ threshold: 1024,
154
+ encodings: ["gzip", "deflate"],
155
+ customTypes: /^text\\/|javascript|json|xml|svg|wasm/,
156
+ });
157
+
158
+ await app.register(fastifyMultipart, {""",
159
+ 1,
160
+ )
161
+
162
+ (root / "src/server/main.ts").write_text(main)
163
+
164
+ pkg = (root / "package.json").read_text()
165
+ if '"@fastify/compress"' not in pkg:
166
+ pkg = pkg.replace(
167
+ '"@fastify/multipart": "^10.0.0",',
168
+ '"@fastify/compress": "^9.2.0",\n "@fastify/multipart": "^10.0.0",',
169
+ 1,
170
+ )
171
+ (root / "package.json").write_text(pkg)
172
+
173
+ # browser shim: application keepalive (proxies ignore WS ping frames)
174
+ shim = (root / "src/browser/shim.ts").read_text()
175
+ shim = shim.replace(
176
+ "const RECONNECT_DELAY_MS = 1_000;",
177
+ "const RECONNECT_DELAY_MS = 400;",
178
+ 1,
179
+ )
180
+
181
+ old_in = """function handleIncomingMessage(message: MainToRendererMessage): void {
182
+ if (message.type === "ipc-main-event") {
183
+ """
184
+ new_in = """function handleIncomingMessage(message: MainToRendererMessage): void {
185
+ const maybeType = (message as { type?: string }).type;
186
+ if (maybeType === "keepalive" || maybeType === "keepalive-ack") {
187
+ return;
188
+ }
189
+ if (message.type === "ipc-main-event") {
190
+ """
191
+ if old_in not in shim:
192
+ raise SystemExit("patch fail: shim handleIncomingMessage")
193
+ shim = shim.replace(old_in, new_in, 1)
194
+
195
+ old_open = """ socket.addEventListener("open", () => {
196
+ flushOutboundQueue();
197
+ });
198
+ """
199
+ new_open = """ socket.addEventListener("open", () => {
200
+ flushOutboundQueue();
201
+ try { socket.send(JSON.stringify({ type: "keepalive", t: Date.now() })); } catch { /* ignore */ }
202
+ });
203
+ """
204
+ if old_open not in shim:
205
+ raise SystemExit("patch fail: shim open")
206
+ shim = shim.replace(old_open, new_open, 1)
207
+
208
+ needle = "ensureSocket();\n\nexport const contextBridge"
209
+ if needle not in shim:
210
+ raise SystemExit("patch fail: shim ensureSocket trailer")
211
+ shim = shim.replace(
212
+ needle,
213
+ """ensureSocket();
214
+
215
+ setInterval(() => {
216
+ if (socket && socket.readyState === WebSocket.OPEN) {
217
+ try { socket.send(JSON.stringify({ type: "keepalive", t: Date.now() })); } catch { /* ignore */ }
218
+ } else {
219
+ ensureSocket();
220
+ }
221
+ }, 12000);
222
+
223
+ document.addEventListener("visibilitychange", () => {
224
+ if (document.visibilityState === "visible") {
225
+ ensureSocket();
226
+ }
227
+ });
228
+ window.addEventListener("online", () => ensureSocket());
229
+ window.addEventListener("pageshow", () => ensureSocket());
230
+
231
+ export const contextBridge""",
232
+ 1,
233
+ )
234
+ (root / "src/browser/shim.ts").write_text(shim)
235
+
236
+ # electron getPath: cache/temp off the bucket, userdata stays persisted
237
+ elec = (root / "src/server/electron/index.ts").read_text()
238
+ old_gp = """ getPath(name: string): string {
239
+ log("app.getPath", [name]);
240
+ return process.cwd();
241
+ },
242
+ """
243
+ new_gp = """ getPath(name: string): string {
244
+ log("app.getPath", [name]);
245
+ const home = process.env.HOME || "/home/user";
246
+ const persist = process.env.CODEX_WEB_USERDATA || home + "/app-data";
247
+ const tmp = process.env.CODEX_WEB_CACHE || "/var/tmp/codex-electron";
248
+ switch (name) {
249
+ case "home":
250
+ return home;
251
+ case "temp":
252
+ case "cache":
253
+ case "userCache":
254
+ case "crashDumps":
255
+ return tmp;
256
+ case "logs":
257
+ return tmp + "/logs";
258
+ case "desktop":
259
+ case "documents":
260
+ case "downloads":
261
+ case "music":
262
+ case "pictures":
263
+ case "videos":
264
+ return home;
265
+ default:
266
+ return persist;
267
+ }
268
+ },
269
+ """
270
+ if old_gp not in elec:
271
+ raise SystemExit("patch fail: electron getPath")
272
+ elec = elec.replace(old_gp, new_gp, 1)
273
+ (root / "src/server/electron/index.ts").write_text(elec)
274
+
275
+ # smaller, faster webview preload bundle
276
+ vite = (root / "vite.browser.config.ts").read_text()
277
+ vite = vite.replace("minify: false,", "minify: true,", 1)
278
+ vite = vite.replace("sourcemap: true,", "sourcemap: false,", 1)
279
+ (root / "vite.browser.config.ts").write_text(vite)
280
+
281
+ print("hf patches applied")
282
+ PY
283
+
284
+ RUN python3 /opt/apply-hf-patches.py \
285
+ && cd /opt/codex-web \
286
+ && npm install --omit=dev \
287
+ && npm install -g @openai/codex \
288
+ && npm cache clean --force \
289
+ && chmod -R a+rX /opt/codex-web /opt/npm-global
290
+
291
+ FROM node:24-slim
292
+
293
+ ENV NPM_CONFIG_PREFIX=/opt/npm-global \
294
+ NPM_CONFIG_UPDATE_NOTIFIER=false \
295
+ NPM_CONFIG_FUND=false \
296
+ NPM_CONFIG_AUDIT=false \
297
+ PATH=/opt/npm-global/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin \
298
+ NODE_ENV=production \
299
+ NODE_OPTIONS=--dns-result-order=ipv4first
300
+
301
+ RUN apt-get update \
302
+ && apt-get install -y --no-install-recommends \
303
+ ca-certificates curl wget git tini sudo gosu \
304
+ python3 python3-pip python3-venv python3-dev \
305
+ gcc g++ make cmake pkg-config \
306
+ unzip zip \
307
+ libssl-dev libffi-dev \
308
+ procps bash \
309
+ ripgrep fd-find \
310
+ && rm -rf /var/lib/apt/lists/* \
311
+ && ln -sf "$(command -v fdfind || true)" /usr/local/bin/fd || true \
312
+ && groupmod -n user node \
313
+ && usermod -l user -d /home/user -m node \
314
+ && usermod -aG sudo user \
315
+ && mkdir -p /home/user/app /data /var/tmp/codex-ephemeral /opt/npm-global \
316
+ && echo "user ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/user \
317
+ && chmod 440 /etc/sudoers.d/user \
318
+ && chown -R user:user /home/user /data /var/tmp/codex-ephemeral
319
+
320
+ COPY --from=build /opt/codex-web /opt/codex-web
321
+ COPY --from=build /opt/npm-global /opt/npm-global
322
+ RUN chown -R user:user /opt/npm-global \
323
+ && chmod -R a+rX /opt/codex-web /opt/npm-global
324
+
325
+ COPY --chmod=0755 <<'EOF' /usr/local/bin/entrypoint
326
+ #!/usr/bin/env bash
327
+ set -euo pipefail
328
+
329
+ DATA_DIR="/data"
330
+ EPHEM="/var/tmp/codex-ephemeral"
331
+ mkdir -p "$EPHEM" /var/tmp/codex-electron /tmp
332
+
333
+ log() { echo "[entrypoint] $*"; }
334
+
335
+ # Persist $HOME onto the bucket; never ls/du/rm -rf large FUSE trees on the boot path.
336
+ if [ -w "$DATA_DIR" ]; then
337
+ log "storage bucket detected at $DATA_DIR β€” persisting the entire home directory."
338
+ PERSIST_HOME="$DATA_DIR/home"
339
+ mkdir -p "$PERSIST_HOME"
340
+
341
+ LISTING="$(timeout 8 ls -A "$PERSIST_HOME" 2>/dev/null || true)"
342
+ if [ -z "$LISTING" ]; then
343
+ log "bucket looks empty β€” seeding from the image's baked-in \$HOME"
344
+ timeout 60 cp -a /home/user/. "$PERSIST_HOME"/ 2>/dev/null \
345
+ || log "WARNING: seed copy timed out or failed β€” continuing"
346
+ fi
347
+ export HOME="$PERSIST_HOME"
348
+ else
349
+ log "WARNING: no writable storage bucket at $DATA_DIR β€” EPHEMERAL storage, lost on restart."
350
+ log "Attach a bucket at mount path '/data', Access: Read & Write, in Settings -> Storage."
351
+ export HOME="${HOME:-/home/user}"
352
+ fi
353
+
354
+ # If started as root: chown the persist tree, bind-mount /usr/local, then re-exec as uid 1000
355
+ # before any Codex files are written so bucket objects are not stuck root-owned.
356
+ if [ "$(id -u)" = "0" ] && [ "${1:-}" != "--as-user" ]; then
357
+ mkdir -p "$HOME/.usr-local" "$HOME/.local" "$HOME/.codex" "$HOME/app-data"
358
+ chown user:user "$HOME" "$HOME/.usr-local" "$HOME/.local" "$HOME/.codex" "$HOME/app-data" 2>/dev/null || true
359
+ if [ -z "$(timeout 5 ls -A "$HOME/.usr-local" 2>/dev/null || true)" ]; then
360
+ timeout 30 cp -a /usr/local/. "$HOME/.usr-local"/ 2>/dev/null || true
361
+ chown -R user:user "$HOME/.usr-local" 2>/dev/null || true
362
+ fi
363
+ if mount --bind "$HOME/.usr-local" /usr/local 2>/dev/null; then
364
+ log "bind-mounted \$HOME/.usr-local -> /usr/local (compilers you install here persist)"
365
+ else
366
+ log "could not bind-mount /usr/local (no CAP_SYS_ADMIN) β€” use \$HOME/.local instead"
367
+ fi
368
+ log "dropping privileges to user uid 1000"
369
+ exec gosu user "$0" --as-user
370
+ fi
371
+
372
+ export CODEX_HOME="$HOME/.codex"
373
+ mkdir -p "$CODEX_HOME" "$HOME/.local/bin" "$HOME/.local/lib" "$HOME/app-data" "$HOME/.usr-local"
374
+
375
+ if ! timeout 15 touch "$HOME/.write-probe" 2>/dev/null; then
376
+ log "FATAL: $HOME is not writable by uid $(id -u) (or the bucket didn't respond within 15s)."
377
+ log "Check Settings -> Storage: Access mode must be Read & Write, then do a full Restart."
378
+ exit 1
379
+ fi
380
+ rm -f "$HOME/.write-probe" || true
381
+
382
+ # Divert only Codex runtime bloat onto fast local disk; the rest of $HOME stays on the bucket.
383
+ # Symlink, not env-var: Codex hardcodes ~/.cache/codex-runtimes and ignores XDG_CACHE_HOME.
384
+ divert_to_ephemeral() {
385
+ local src="$1"
386
+ local dest="$2"
387
+ mkdir -p "$(dirname "$src")" "$dest"
388
+ if [ -L "$src" ]; then
389
+ ln -sfn "$dest" "$src"
390
+ return 0
391
+ fi
392
+ if [ -e "$src" ]; then
393
+ local stale="${src}.STALE.$$"
394
+ if mv "$src" "$stale" 2>/dev/null; then
395
+ log "moved existing $(basename "$src") off the live path β€” deleting in background"
396
+ ( nice rm -rf "$stale" >/dev/null 2>&1 || true ) &
397
+ else
398
+ log "WARNING: could not rename $src β€” leaving it and still linking"
399
+ fi
400
+ fi
401
+ ln -sfn "$dest" "$src"
402
+ }
403
+
404
+ mkdir -p "$HOME/.cache" "$HOME/.local/state/codex"
405
+ divert_to_ephemeral "$HOME/.cache/codex-runtimes" "$EPHEM/codex-runtimes"
406
+ divert_to_ephemeral "$HOME/.cache/codex-primary-runtime" "$EPHEM/codex-primary-runtime"
407
+ divert_to_ephemeral "$HOME/.local/state/codex/logs" "$EPHEM/codex-logs-xdg"
408
+
409
+ if compgen -G "$HOME/.cache/codex-runtimes.STALE*" >/dev/null \
410
+ || compgen -G "$HOME/.cache/codex-primary-runtime.STALE*" >/dev/null; then
411
+ log "sweeping leftover STALE runtime dirs in the background"
412
+ ( nice rm -rf "$HOME/.cache"/codex-runtimes.STALE* \
413
+ "$HOME/.cache"/codex-primary-runtime.STALE* >/dev/null 2>&1 || true ) &
414
+ fi
415
+
416
+ # Do NOT export XDG_CACHE_HOME / XDG_STATE_HOME to /tmp β€” that would throw
417
+ # non-Codex caches (pip, huggingface, etc.) off the bucket too.
418
+ export CODEX_WEB_USERDATA="$HOME/app-data"
419
+ export CODEX_WEB_CACHE="/var/tmp/codex-electron"
420
+ export PYTHONUSERBASE="$HOME/.local"
421
+ export PIP_USER=1
422
+ export npm_config_prefix="$HOME/.local"
423
+ export NPM_CONFIG_PREFIX="$HOME/.local"
424
+ export CARGO_HOME="$HOME/.cargo"
425
+ export RUSTUP_HOME="$HOME/.rustup"
426
+ export GOPATH="$HOME/go"
427
+ export PATH="$HOME/.local/bin:$HOME/.cargo/bin:$HOME/go/bin:/opt/npm-global/bin:$PATH"
428
+
429
+ mkdir -p "$HOME/.local/bin" "$CARGO_HOME/bin" "$GOPATH/bin" "$CODEX_WEB_USERDATA"
430
+
431
+ # Codex CLI: image ships a working binary; only install if missing.
432
+ if ! command -v codex >/dev/null 2>&1; then
433
+ log "codex missing from PATH β€” installing latest into \$HOME/.local ..."
434
+ npm install -g @openai/codex \
435
+ || { log "FATAL: could not install @openai/codex"; exit 1; }
436
+ fi
437
+
438
+ export CODEX_CLI_PATH="$(command -v codex)"
439
+ if [ -z "${CODEX_CLI_PATH}" ]; then
440
+ log "FATAL: 'codex' isn't on PATH."
441
+ exit 1
442
+ fi
443
+ log "codex: $($CODEX_CLI_PATH --version 2>/dev/null || echo unknown) at $CODEX_CLI_PATH"
444
+
445
+ if [ -n "${OPENAI_API_KEY:-}" ] && [ ! -f "$CODEX_HOME/auth.json" ]; then
446
+ log "logging in to Codex from \$OPENAI_API_KEY ..."
447
+ printenv OPENAI_API_KEY | codex login --with-api-key \
448
+ || log "WARNING: codex login failed β€” check that OPENAI_API_KEY is valid."
449
+ elif [ -z "${OPENAI_API_KEY:-}" ] && [ ! -f "$CODEX_HOME/auth.json" ]; then
450
+ log "WARNING: no OPENAI_API_KEY and no existing login β€” Codex won't be usable until set."
451
+ fi
452
+
453
+ (
454
+ set +e
455
+ sleep 8
456
+ LATEST="$(timeout 40 npm view @openai/codex version 2>/dev/null || true)"
457
+ CURRENT="$(codex --version 2>/dev/null | awk '{print $NF}' || true)"
458
+ if [ -n "$LATEST" ] && [ -n "$CURRENT" ] && [ "$LATEST" != "$CURRENT" ]; then
459
+ log "codex: $CURRENT -> $LATEST (background, into \$HOME/.local)"
460
+ npm install -g "@openai/codex@${LATEST}" \
461
+ && log "codex: now $(codex --version 2>/dev/null)"
462
+ fi
463
+ ) &
464
+
465
+ (
466
+ set +e
467
+ SEL="$HOME/.dpkg-selections"
468
+ if [ -f "$SEL" ] && command -v sudo >/dev/null 2>&1; then
469
+ log "replaying persisted apt selections in the background"
470
+ timeout 90 sudo apt-get update -qq
471
+ timeout 300 sudo dpkg --set-selections < "$SEL"
472
+ timeout 600 sudo apt-get dselect-upgrade -y -qq
473
+ fi
474
+ while true; do
475
+ if command -v dpkg >/dev/null 2>&1; then
476
+ dpkg --get-selections > "$SEL.tmp" 2>/dev/null && mv "$SEL.tmp" "$SEL"
477
+ fi
478
+ sleep 180
479
+ done
480
+ ) &
481
+
482
+ cd "$CODEX_WEB_USERDATA"
483
+ log "serving on 0.0.0.0:7860 β€” no auth in front of this (codex-web has none by design)"
484
+ exec node /opt/codex-web/src/server/main.js --host 0.0.0.0 --port 7860
485
+ EOF
486
+
487
+ ENV HOME=/home/user
488
+ WORKDIR /home/user/app
489
+
490
+ EXPOSE 7860
491
+
492
+ ENTRYPOINT ["/usr/bin/tini", "--", "/usr/local/bin/entrypoint"]