-
-
Notifications
You must be signed in to change notification settings - Fork 260
Expand file tree
/
Copy pathDockerfile
More file actions
664 lines (610 loc) · 41.4 KB
/
Copy pathDockerfile
File metadata and controls
664 lines (610 loc) · 41.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
# ==============================================================================
# HolyClaude — Pre-configured Docker Environment for Claude Code CLI + CloudCLI
# https://github.com/coderluii/holyclaude
#
# Build variants:
# docker build -t holyclaude . # full (default)
# docker build --build-arg VARIANT=slim -t holyclaude:slim .
# ==============================================================================
FROM golang:1.26.5-bookworm@sha256:1ecb7edf62a0408027bd5729dfd6b1b8766e578e8df93995b225dfd0944eb651 AS esbuild-builder
ARG TARGETARCH
RUN case "$TARGETARCH" in amd64) ;; arm64) ;; *) echo "Unsupported TARGETARCH: $TARGETARCH" >&2; exit 1;; esac; \
set -eux; \
for ESBUILD_VERSION in 0.15.18 0.18.20 0.25.12; do \
mkdir -p "/out/${ESBUILD_VERSION}"; \
CGO_ENABLED=0 GOOS=linux GOARCH="$TARGETARCH" GOBIN="/out/${ESBUILD_VERSION}" \
go install "github.com/evanw/esbuild/cmd/esbuild@v${ESBUILD_VERSION}"; \
test "$("/out/${ESBUILD_VERSION}/esbuild" --version)" = "$ESBUILD_VERSION"; \
done
FROM node:26.5.1-bookworm-slim@sha256:9e6f9357d371591e32ab6f2d8a26d63bdd0d17c29eee3f4f3e7e454d9634bf73
ARG HOLYCLAUDE_VERSION=1.5.6
LABEL org.opencontainers.image.source=https://github.com/CoderLuii/HolyClaude
LABEL org.opencontainers.image.version=${HOLYCLAUDE_VERSION}
# ---------- Build args ----------
ARG S6_OVERLAY_VERSION=3.2.3.2
ARG S6_NOARCH_SHA256=5379750ed30a84bbd2e2dd74847ba6b5bd29cd0b2e3ea2ec58049b57eb2eda12
ARG S6_ARCHIVE_SHA256_AMD64=e6befcc96a437a3831386ecfc51808c5d3e939dc5fe3c02ae9284599e8aa2408
ARG S6_ARCHIVE_SHA256_ARM64=b17f17a82e7a515c682a91edaf2ffdabb73f891981b6c1fd712115693a2f8b4c
ARG FZF_VERSION=0.74.1
ARG FZF_ARCHIVE_SHA256_AMD64=df53438be5f51e151bb4044d78fda72bdfe209e3ecd2baecae48e8dea370c81b
ARG FZF_ARCHIVE_SHA256_ARM64=f22204dd1a091d43e102268d062fd53b47133c8d8581671ee5eb225b75e31183
ARG CHROMIUM_DEBIAN_VERSION=151.0.7922.71-1~deb12u1
ARG CHROMIUM_PACKAGE_SHA256_AMD64=455423ff7608b4a2af8ef6e66596ce86d313ae9e055381feee9e39df9f6165ef
ARG CHROMIUM_PACKAGE_SHA256_ARM64=1abbdfc529cd7b8576ec41b2f2aa4660888f7fd3efd6579b3d79cfc30bc0389d
ARG CHROMIUM_COMMON_PACKAGE_SHA256_AMD64=a99c21a89cac35e18997df511d4173cfb7bc57ea0312e88b0c3b99e564050938
ARG CHROMIUM_COMMON_PACKAGE_SHA256_ARM64=d26cdb3cc2ed1080a499603f5f0483ee9f377c9a753a8469dcf5be2004e74e8d
ARG CHROMIUM_SANDBOX_PACKAGE_SHA256_AMD64=d3ae37073eb000326047e9d352beb32333beb6d0b1655dfe389ff2ba2a26a9c9
ARG CHROMIUM_SANDBOX_PACKAGE_SHA256_ARM64=99c6c715559c7f6fd6f116880d3427bb1289f4c33ebcbfa51127c1ae7230e4eb
ARG CLAUDE_CODE_VERSION=2.1.220
ARG CLAUDE_INSTALLER_SHA256=cde4f1702d3b1695f92b73d26888364e17bca476e17f0fd676484c951d36c125
ARG CLAUDE_BINARY_SHA256_AMD64=674f61f20ff306f3100cf9200e4c36c4b70278b5bef2884549819b942a89c863
ARG CLAUDE_BINARY_SHA256_ARM64=159e4a51d796f3bf14677577100f7efb845611b1ceaf0c30cbd8d4650d942185
ARG JUNIE_VERSION=2470.4
ARG JUNIE_ARCHIVE_SHA256_AMD64=661dba7d55e097ae0eb62ff2475b4e9fe7a59d8e25560d8c1981aad85901b60c
ARG JUNIE_ARCHIVE_SHA256_ARM64=976c6f974598bb34197f434dd041cfbb1cd663d95702ee3260bcb07815a0f630
ARG CURSOR_BUILD_ID=2026.07.23-e383d2b
ARG CURSOR_ARCHIVE_SHA256_AMD64=702ad595213bee5df0268be9f80a19f29fcceaa2a42fc55e39f2b5199051f0c4
ARG CURSOR_ARCHIVE_SHA256_ARM64=f40b99647cb24e0da885e97620a2048034f1fe8961910d573d827d77c4d26dcb
ARG CURSOR_LAUNCHER_SHA256=eed61c5224668c9236334c4c68936a16aecc37374b592f59e31eb50433817831
ARG CURSOR_NODE_SHA256_AMD64=e0e46d3a1c0667117303412647cafcbcefb1be7612493015ec8fd6b7440162a4
ARG CURSOR_NODE_SHA256_ARM64=47befb5f57df96771ce343d6293349ecf4d46c91110b626423ec3a49d2fee7c1
ARG SETUPTOOLS_VERSION=83.0.0
ARG SETUPTOOLS_WHEEL_SHA256=29b23c360f22f414dc7336bb39178cc7bcbf6021ed2733cde173f09dba19abb3
ARG PISCINA_VERSION=4.9.3
ARG PISCINA_ARCHIVE_SHA256=5207b79c42ff172230529f5aa355f17d855b1481836bc841db19c6081fc5ec1e
ARG BRACE_EXPANSION_VERSION=5.0.9
ARG BRACE_EXPANSION_ARCHIVE_SHA256=5d06001fddd25cbee90c96db4dc5b7b57711b984c3141e28d10f143deb52dbaf
ARG GLOB_VERSION=11.1.0
ARG GLOB_ARCHIVE_SHA256=8816e244d245d86a1b8adf9ed0bf61c9665dbb1ee7b00dd6b3283f3ac0393bfb
ARG JS_YAML_VERSION=4.3.0
ARG JS_YAML_ARCHIVE_SHA256=8594ee34496dd2e41ec934fd202843dc993be9ab2d7d5d47579146962fbdfae6
ARG MINIMATCH_5_VERSION=5.1.9
ARG MINIMATCH_5_ARCHIVE_SHA256=67e7dacfba9fcabb6ac661620b67e6c22600b4aa56ffa14431cbdfdeebbd4cfe
ARG MINIMATCH_10_VERSION=10.2.6
ARG MINIMATCH_10_ARCHIVE_SHA256=5a3d2c8074a28229665727e47b8a1090941856a7962905efe05d20d3760355f8
ARG NODE_FORGE_VERSION=1.4.0
ARG NODE_FORGE_ARCHIVE_SHA256=bf9d7ca0d774235354697bd4b5e642af6505e7ce2066762c3b855138cf870820
ARG PATH_TO_REGEXP_6_VERSION=6.3.0
ARG PATH_TO_REGEXP_6_ARCHIVE_SHA256=da302284390341278d3dad1014f2043cf844f6a2163aa8dc5686d321d82742e6
ARG PATH_TO_REGEXP_8_VERSION=8.4.2
ARG PATH_TO_REGEXP_8_ARCHIVE_SHA256=e8712a9c53b0a2a27cfecc7b80c54df92afb4643c01351e2b2ebb7784bcabd78
ARG WS_VERSION=8.21.1
ARG WS_ARCHIVE_SHA256=bb0f7e58ba1f64746672734d36175fe185f226491e336abc0743e2a8f4472ec1
ARG AZURE_CLI_VERSION=2.88.0-1~bookworm
ARG AZURE_CLI_INSTALLER_SHA256=01fada4dafe903fa6edae138d3e3ca2e6e4295d7c8a35e48632bba4aa9dbe9d9
ARG GITHUB_CLI_VERSION=2.97.0
ARG GITHUB_CLI_PACKAGE_SHA256_AMD64=7c7fa3bb890db0934baf65910d97b8c0fa437b2e590f7f7daf6bdf82c5c486d7
ARG GITHUB_CLI_PACKAGE_SHA256_ARM64=0ba7a76739c865d82ebde24667d875d9b8caa55db47c7597c24accdd4defd2bb
ARG NODE_TAR_VERSION=7.5.22
ARG NODE_TAR_SHA256=b792c2d1c7fc770910522ca1ffc29eee02ee38de4fa3a01e7832eb705879c6c6
ARG TARGETARCH
ARG VARIANT=full
# ---------- Environment ----------
ENV DEBIAN_FRONTEND=noninteractive \
LANG=en_US.UTF-8 \
LC_ALL=en_US.UTF-8 \
DISPLAY=:99 \
DBUS_SESSION_BUS_ADDRESS=disabled: \
CHROMIUM_FLAGS="--no-sandbox --disable-gpu --disable-dev-shm-usage" \
CHROME_PATH=/usr/bin/chromium \
PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium \
NODE_PATH=/usr/local/lib/node_modules
# ---------- s6-overlay v3 (multi-arch) ----------
RUN apt-get update && apt-get install -y --no-install-recommends xz-utils curl ca-certificates && rm -rf /var/lib/apt/lists/*
RUN S6_ARCH=$(case "$TARGETARCH" in amd64) echo "x86_64";; arm64) echo "aarch64";; *) echo "Unsupported TARGETARCH: $TARGETARCH" >&2; exit 1;; esac) && \
S6_ARCH_SHA256=$(case "$TARGETARCH" in amd64) echo "$S6_ARCHIVE_SHA256_AMD64";; arm64) echo "$S6_ARCHIVE_SHA256_ARM64";; *) echo "Unsupported TARGETARCH: $TARGETARCH" >&2; exit 1;; esac) && \
for S6_ASSET in noarch "$S6_ARCH"; do \
S6_EXPECTED_SHA256=$(case "$S6_ASSET" in noarch) echo "$S6_NOARCH_SHA256";; *) echo "$S6_ARCH_SHA256";; esac); \
curl -fsSL -o "/tmp/s6-overlay-${S6_ASSET}.tar.xz" \
"https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-${S6_ASSET}.tar.xz"; \
curl -fsSL -o "/tmp/s6-overlay-${S6_ASSET}.tar.xz.sha256" \
"https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-${S6_ASSET}.tar.xz.sha256"; \
test "$(cut -d' ' -f1 "/tmp/s6-overlay-${S6_ASSET}.tar.xz.sha256")" = "$S6_EXPECTED_SHA256"; \
echo "$S6_EXPECTED_SHA256 /tmp/s6-overlay-${S6_ASSET}.tar.xz" | sha256sum -c -; \
done && \
tar -C / -Jxpf /tmp/s6-overlay-noarch.tar.xz && \
tar -C / -Jxpf "/tmp/s6-overlay-${S6_ARCH}.tar.xz" && \
rm /tmp/s6-overlay-*.tar.xz /tmp/s6-overlay-*.tar.xz.sha256
# ---------- System packages (always installed) ----------
RUN apt-get update && apt-get install -y --no-install-recommends \
# Core utilities
git curl wget jq ripgrep fd-find unzip zip tree tmux bat bubblewrap \
# Build tools
build-essential pkg-config python3 python3-pip python3-venv \
# Fonts
fonts-liberation2 fonts-dejavu-core fonts-noto-core fonts-noto-color-emoji fonts-inter \
# Locale support
locales \
# Debugging tools
strace lsof iproute2 procps htop \
# Database CLI tools
postgresql-client redis-tools sqlite3 \
# SSH/Mosh remote shell support (disabled by default)
openssh-client openssh-server mosh \
# Xvfb for headless Chrome
xvfb \
# Image processing
imagemagick \
# Sudo
sudo \
&& rm -rf /var/lib/apt/lists/*
# ---------- Browser runtime (checksum-verified Bookworm security packages) ----------
RUN set -eux; \
case "$TARGETARCH" in amd64) DEB_ARCH=amd64; CHROMIUM_PACKAGE_SHA256="$CHROMIUM_PACKAGE_SHA256_AMD64"; CHROMIUM_COMMON_PACKAGE_SHA256="$CHROMIUM_COMMON_PACKAGE_SHA256_AMD64"; CHROMIUM_SANDBOX_PACKAGE_SHA256="$CHROMIUM_SANDBOX_PACKAGE_SHA256_AMD64";; arm64) DEB_ARCH=arm64; CHROMIUM_PACKAGE_SHA256="$CHROMIUM_PACKAGE_SHA256_ARM64"; CHROMIUM_COMMON_PACKAGE_SHA256="$CHROMIUM_COMMON_PACKAGE_SHA256_ARM64"; CHROMIUM_SANDBOX_PACKAGE_SHA256="$CHROMIUM_SANDBOX_PACKAGE_SHA256_ARM64";; *) echo "Unsupported TARGETARCH: $TARGETARCH" >&2; exit 1;; esac; \
mkdir -p /tmp/chromium-debs; \
cd /tmp/chromium-debs; \
apt-get update; \
apt-get download \
"chromium=${CHROMIUM_DEBIAN_VERSION}" \
"chromium-common=${CHROMIUM_DEBIAN_VERSION}" \
"chromium-sandbox=${CHROMIUM_DEBIAN_VERSION}"; \
printf '%s %s\n' \
"$CHROMIUM_PACKAGE_SHA256" "chromium_${CHROMIUM_DEBIAN_VERSION}_${DEB_ARCH}.deb" \
"$CHROMIUM_COMMON_PACKAGE_SHA256" "chromium-common_${CHROMIUM_DEBIAN_VERSION}_${DEB_ARCH}.deb" \
"$CHROMIUM_SANDBOX_PACKAGE_SHA256" "chromium-sandbox_${CHROMIUM_DEBIAN_VERSION}_${DEB_ARCH}.deb" \
| sha256sum -c -; \
apt-get install -y --no-install-recommends ./*.deb; \
test "$(dpkg-query -W -f='${Version}' chromium)" = "$CHROMIUM_DEBIAN_VERSION"; \
test "$(dpkg-query -W -f='${Version}' chromium-common)" = "$CHROMIUM_DEBIAN_VERSION"; \
test "$(dpkg-query -W -f='${Version}' chromium-sandbox)" = "$CHROMIUM_DEBIAN_VERSION"; \
cd /; \
rm -rf /tmp/chromium-debs /var/lib/apt/lists/*
# ---------- fzf (official multi-arch release) ----------
RUN FZF_ARCH=$(case "$TARGETARCH" in amd64) echo "amd64";; arm64) echo "arm64";; *) echo "Unsupported TARGETARCH: $TARGETARCH" >&2; exit 1;; esac) && \
FZF_ARCHIVE_SHA256=$(case "$TARGETARCH" in amd64) echo "$FZF_ARCHIVE_SHA256_AMD64";; arm64) echo "$FZF_ARCHIVE_SHA256_ARM64";; *) echo "Unsupported TARGETARCH: $TARGETARCH" >&2; exit 1;; esac) && \
FZF_ASSET="fzf-${FZF_VERSION}-linux_${FZF_ARCH}.tar.gz" && \
curl -fsSL -o "/tmp/${FZF_ASSET}" \
"https://github.com/junegunn/fzf/releases/download/v${FZF_VERSION}/${FZF_ASSET}" && \
curl -fsSL -o /tmp/fzf-checksums.txt \
"https://github.com/junegunn/fzf/releases/download/v${FZF_VERSION}/fzf_${FZF_VERSION}_checksums.txt" && \
test "$(grep -F " ${FZF_ASSET}" /tmp/fzf-checksums.txt | cut -d' ' -f1)" = "$FZF_ARCHIVE_SHA256" && \
echo "$FZF_ARCHIVE_SHA256 /tmp/${FZF_ASSET}" | sha256sum -c - && \
tar -xzf "/tmp/${FZF_ASSET}" -C /usr/local/bin fzf && \
test "$(/usr/local/bin/fzf --version | awk '{print $1}')" = "$FZF_VERSION" && \
rm -f "/tmp/${FZF_ASSET}" /tmp/fzf-checksums.txt
RUN rm -f /etc/ssh/ssh_host_*_key /etc/ssh/ssh_host_*_key.pub
# ---------- bubblewrap setuid (Codex CLI sandbox on restricted kernels) ----------
RUN test -x /usr/bin/bwrap && chown root:root /usr/bin/bwrap && chmod 4755 /usr/bin/bwrap && test "$(stat -c '%a %u %g' /usr/bin/bwrap)" = "4755 0 0"
# ---------- Full-only system packages ----------
RUN if [ "$VARIANT" = "full" ]; then \
apt-get update && apt-get install -y --no-install-recommends \
pandoc ffmpeg libvips-dev \
&& rm -rf /var/lib/apt/lists/*; \
fi
# ---------- Azure CLI (full only) ----------
RUN if [ "$VARIANT" = "full" ]; then \
curl -fsSL https://aka.ms/InstallAzureCLIDeb -o /tmp/azure-cli-install.sh && \
echo "$AZURE_CLI_INSTALLER_SHA256 /tmp/azure-cli-install.sh" | sha256sum -c - && \
bash /tmp/azure-cli-install.sh && \
test "$(dpkg-query -W -f='${Version}' azure-cli)" = "$AZURE_CLI_VERSION" && \
rm -f /tmp/azure-cli-install.sh && rm -rf /var/lib/apt/lists/*; \
fi
# ---------- GitHub CLI ----------
RUN GITHUB_CLI_ARCH=$(case "$TARGETARCH" in amd64) echo "amd64";; arm64) echo "arm64";; *) echo "Unsupported TARGETARCH: $TARGETARCH" >&2; exit 1;; esac) && \
GITHUB_CLI_PACKAGE_SHA256=$(case "$TARGETARCH" in amd64) echo "$GITHUB_CLI_PACKAGE_SHA256_AMD64";; arm64) echo "$GITHUB_CLI_PACKAGE_SHA256_ARM64";; *) echo "Unsupported TARGETARCH: $TARGETARCH" >&2; exit 1;; esac) && \
GITHUB_CLI_PACKAGE="gh_${GITHUB_CLI_VERSION}_linux_${GITHUB_CLI_ARCH}.deb" && \
curl -fsSL -o "/tmp/${GITHUB_CLI_PACKAGE}" \
"https://github.com/cli/cli/releases/download/v${GITHUB_CLI_VERSION}/${GITHUB_CLI_PACKAGE}" && \
echo "$GITHUB_CLI_PACKAGE_SHA256 /tmp/${GITHUB_CLI_PACKAGE}" | sha256sum -c - && \
apt-get update && apt-get install -y "/tmp/${GITHUB_CLI_PACKAGE}" && \
test "$(dpkg-query -W -f='${Version}' gh)" = "$GITHUB_CLI_VERSION" && \
rm -f "/tmp/${GITHUB_CLI_PACKAGE}" && \
rm -rf /var/lib/apt/lists/*
# ---------- bat symlink (Debian names it batcat) ----------
RUN ln -sf /usr/bin/batcat /usr/local/bin/bat 2>/dev/null || true
# ---------- Locale configuration ----------
RUN sed -i '/en_US.UTF-8/s/^# //g' /etc/locale.gen && locale-gen
# ---------- Create claude user ----------
# The official Node slim image already has UID 1000 as 'node' — rename it to 'claude'
RUN usermod -l claude -d /home/claude -m node && \
groupmod -n claude node && \
mkdir -p /home/claude/.cloudcli && \
chown claude:claude /home/claude/.cloudcli && \
echo "claude ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/claude && \
chmod 0440 /etc/sudoers.d/claude
# ---------- Claude Code CLI (native installer) ----------
# CRITICAL: WORKDIR must be non-root-owned or the installer hangs
WORKDIR /workspace
USER claude
RUN CLAUDE_BINARY_SHA256=$(case "$TARGETARCH" in amd64) echo "$CLAUDE_BINARY_SHA256_AMD64";; arm64) echo "$CLAUDE_BINARY_SHA256_ARM64";; *) echo "Unsupported TARGETARCH: $TARGETARCH" >&2; exit 1;; esac) && \
curl -fsSL https://claude.ai/install.sh -o /tmp/claude-install.sh && \
echo "$CLAUDE_INSTALLER_SHA256 /tmp/claude-install.sh" | sha256sum -c - && \
bash /tmp/claude-install.sh "$CLAUDE_CODE_VERSION" && \
test "$(/home/claude/.local/bin/claude --version | awk '{print $1}')" = "$CLAUDE_CODE_VERSION" && \
echo "$CLAUDE_BINARY_SHA256 $(readlink -f /home/claude/.local/bin/claude)" | sha256sum -c - && \
rm -f /tmp/claude-install.sh
USER root
RUN rm -f /home/claude/.claude.json
ENV PATH="/home/claude/.local/bin:${PATH}"
# ---------- npm global packages (slim — always installed) ----------
RUN npm install -g npm@11.19.0 && \
test "$(npm --version)" = "11.19.0"
RUN PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 npm i -g \
playwright@1.61.0 \
typescript@6.0.3 tsx@4.23.1 \
pnpm@11.18.0 \
vite@8.2.0 esbuild@0.28.1 \
eslint@10.8.0 prettier@3.9.6 \
serve@14.2.6 nodemon@3.1.14 concurrently@10.0.4 \
dotenv-cli@11.0.0
# ---------- npm global packages (full only) ----------
RUN if [ "$VARIANT" = "full" ]; then \
npm i -g \
wrangler@4.116.0 vercel@54.21.1 netlify-cli@26.2.0 \
pm2@7.0.3 \
prisma@7.9.1 drizzle-kit@0.31.10 \
eas-cli@20.5.1 \
lighthouse@13.4.1 @lhci/cli@0.15.1 \
sharp-cli@5.2.0 json-server@1.0.0-beta.15 http-server@14.1.1 \
@marp-team/marp-cli@4.5.0 && \
npm i -g --legacy-peer-deps @cloudflare/next-on-pages@1.13.16; \
fi
# EAS 20 and Vercel 54 pin tar 7.5.7. Replace only their installed copies
# with the checksum-bound fix for CVE-2026-59873, then update exact metadata.
COPY scripts/patch-global-node-tar.mjs /tmp/patch-global-node-tar.mjs
RUN if [ "$VARIANT" = "full" ]; then \
node /tmp/patch-global-node-tar.mjs --root / --check-baseline && \
curl -fsSL "https://registry.npmjs.org/tar/-/tar-${NODE_TAR_VERSION}.tgz" -o /tmp/node-tar.tgz && \
echo "$NODE_TAR_SHA256 /tmp/node-tar.tgz" | sha256sum -c - && \
for target in \
/usr/local/lib/node_modules/eas-cli/node_modules/tar \
/usr/local/lib/node_modules/vercel/node_modules/tar; do \
rm -rf "$target" && \
mkdir -p "$target" && \
tar -xzf /tmp/node-tar.tgz --strip-components=1 -C "$target"; \
done && \
node /tmp/patch-global-node-tar.mjs --root / && \
test "$(node -p "require('/usr/local/lib/node_modules/eas-cli/node_modules/tar/package.json').version")" = "$NODE_TAR_VERSION" && \
test "$(node -p "require('/usr/local/lib/node_modules/vercel/node_modules/tar/package.json').version")" = "$NODE_TAR_VERSION" && \
node -e "for (const path of ['/usr/local/lib/node_modules/eas-cli/node_modules/tar', '/usr/local/lib/node_modules/vercel/node_modules/tar']) { if (typeof require(path).list !== 'function') throw new Error('invalid tar module at ' + path); }" && \
eas --version >/dev/null && \
vercel --version >/dev/null && \
rm -f /tmp/node-tar.tgz; \
fi
# Netlify CLI 26.2.0 bundles an optional local Go/Rust functions proxy built
# with Go 1.16.7. Keep the deployment CLI, but remove that stale executable.
RUN if [ "$VARIANT" = "full" ]; then \
NETLIFY_PROXY_ARCH=$(case "$TARGETARCH" in amd64) echo "x64";; arm64) echo "arm64";; *) echo "Unsupported TARGETARCH: $TARGETARCH" >&2; exit 1;; esac) && \
NETLIFY_PROXY_ROOT="/usr/local/lib/node_modules/netlify-cli/node_modules/@netlify/local-functions-proxy-linux-${NETLIFY_PROXY_ARCH}" && \
test "$(node -p "require('${NETLIFY_PROXY_ROOT}/package.json').version")" = "1.1.1" && \
test -x "$NETLIFY_PROXY_ROOT/bin/local-functions-proxy" && \
rm -f "$NETLIFY_PROXY_ROOT/bin/local-functions-proxy" && \
test ! -e "$NETLIFY_PROXY_ROOT/bin/local-functions-proxy" && \
test "$(node -p "require('/usr/local/lib/node_modules/netlify-cli/package.json').version")" = "26.2.0" && \
netlify --version >/dev/null; \
fi
# Rebuild the exact esbuild versions retained by full-only tools with the
# pinned Go toolchain, replacing old upstream native executables only.
COPY --from=esbuild-builder /out/0.15.18/esbuild /tmp/esbuild-0.15.18
COPY --from=esbuild-builder /out/0.18.20/esbuild /tmp/esbuild-0.18.20
COPY --from=esbuild-builder /out/0.25.12/esbuild /tmp/esbuild-0.25.12
RUN if [ "$VARIANT" = "full" ]; then \
ESBUILD_PACKAGE_ARCH=$(case "$TARGETARCH" in amd64) echo "linux-x64";; arm64) echo "linux-arm64";; *) echo "Unsupported TARGETARCH: $TARGETARCH" >&2; exit 1;; esac) && \
install -m 0755 /tmp/esbuild-0.15.18 \
/usr/local/lib/node_modules/@cloudflare/next-on-pages/node_modules/esbuild/bin/esbuild && \
install -m 0755 /tmp/esbuild-0.18.20 \
"/usr/local/lib/node_modules/drizzle-kit/node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/${ESBUILD_PACKAGE_ARCH}/bin/esbuild" && \
install -m 0755 /tmp/esbuild-0.25.12 \
"/usr/local/lib/node_modules/drizzle-kit/node_modules/@esbuild/${ESBUILD_PACKAGE_ARCH}/bin/esbuild" && \
test "$(/usr/local/lib/node_modules/@cloudflare/next-on-pages/node_modules/esbuild/bin/esbuild --version)" = "0.15.18" && \
test "$(/usr/local/lib/node_modules/drizzle-kit/node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/${ESBUILD_PACKAGE_ARCH}/bin/esbuild --version)" = "0.18.20" && \
test "$(/usr/local/lib/node_modules/drizzle-kit/node_modules/@esbuild/${ESBUILD_PACKAGE_ARCH}/bin/esbuild --version)" = "0.25.12"; \
fi && \
rm -f /tmp/esbuild-0.15.18 /tmp/esbuild-0.18.20 /tmp/esbuild-0.25.12
# ---------- Python packages (slim — always installed) ----------
RUN pip install --no-cache-dir --break-system-packages \
requests==2.34.2 httpx==0.28.1 beautifulsoup4==4.15.0 lxml==6.1.1 \
Pillow==12.3.0 \
pandas==3.0.5 numpy==2.4.6 \
openpyxl==3.1.5 python-docx==1.2.0 \
jinja2==3.1.6 pyyaml==6.0.3 python-dotenv==1.2.2 markdown==3.10.3 \
rich==15.0.0 click==8.4.2 tqdm==4.70.0 \
'desloppify[full]==1.0' bandit==1.9.4 defusedxml==0.7.1 \
tree-sitter==0.26.0 tree-sitter-language-pack==1.6.2 stevedore==5.9.0 \
playwright==1.61.0 \
apprise==1.12.0
COPY scripts/holyclaude-chromium /usr/local/bin/holyclaude-chromium
RUN test "$(dpkg-query -W -f='${Version}' chromium)" = "$CHROMIUM_DEBIAN_VERSION" && \
test "$(dpkg-query -W -f='${Version}' chromium-common)" = "$CHROMIUM_DEBIAN_VERSION" && \
test "$(dpkg-query -W -f='${Version}' chromium-sandbox)" = "$CHROMIUM_DEBIAN_VERSION" && \
test -x /usr/lib/chromium/chromium && \
chmod +x /usr/local/bin/holyclaude-chromium && \
ln -sf /usr/local/bin/holyclaude-chromium /usr/bin/chromium && \
test "$(node -p "require('/usr/local/lib/node_modules/playwright/package.json').version")" = "1.61.0" && \
test "$(python3 -c "import importlib.metadata; print(importlib.metadata.version('playwright'))")" = "1.61.0" && \
test "$(/usr/bin/chromium --version | awk '{print $2}')" = "${CHROMIUM_DEBIAN_VERSION%%-*}" && \
runuser -u claude -- test -r /usr/lib/chromium/chromium && \
runuser -u claude -- test -x /usr/lib/chromium/chromium && \
runuser -u claude -- /usr/bin/chromium --version
# ---------- Python packages (full only) ----------
RUN if [ "$VARIANT" = "full" ]; then \
pip install --no-cache-dir --break-system-packages \
reportlab==5.0.0 weasyprint==69.0 cairosvg==2.9.0 fpdf2==2.8.7 PyMuPDF==1.28.0 img2pdf==0.6.3 \
xlsxwriter==3.2.9 xlrd==2.0.2 \
matplotlib==3.11.1 seaborn==0.13.2 \
python-pptx==1.0.2 \
fastapi==0.141.1 uvicorn==0.52.0; \
fi
# Replace Bookworm's runtime setuptools copy after all image packages are built.
RUN curl -fsSL \
"https://files.pythonhosted.org/packages/5d/40/e1e72872c6354b306daef1703549e8e83b4d43cfea356311bf722a043752/setuptools-${SETUPTOOLS_VERSION}-py3-none-any.whl" \
-o "/tmp/setuptools-${SETUPTOOLS_VERSION}-py3-none-any.whl" && \
echo "$SETUPTOOLS_WHEEL_SHA256 /tmp/setuptools-${SETUPTOOLS_VERSION}-py3-none-any.whl" | sha256sum -c - && \
pip install --no-cache-dir --no-deps --break-system-packages "/tmp/setuptools-${SETUPTOOLS_VERSION}-py3-none-any.whl" && \
rm -rf /usr/lib/python3/dist-packages/setuptools \
/usr/lib/python3/dist-packages/setuptools-66.1.1.egg-info \
/usr/lib/python3/dist-packages/pkg_resources \
/usr/lib/python3/dist-packages/_distutils_hack && \
test "$(python3 -c "import importlib.metadata; print(importlib.metadata.version('setuptools'))")" = "$SETUPTOOLS_VERSION" && \
python3 -m pip --version >/dev/null && \
rm -f "/tmp/setuptools-${SETUPTOOLS_VERSION}-py3-none-any.whl"
# ---------- AI CLI providers ----------
RUN npm i -g @google/gemini-cli@0.53.0 @openai/codex@0.146.0 task-master-ai@0.43.1
USER claude
RUN CURSOR_ASSET_ARCH=$(case "$TARGETARCH" in amd64) echo "x64";; arm64) echo "arm64";; *) echo "Unsupported TARGETARCH: $TARGETARCH" >&2; exit 1;; esac) && \
CURSOR_ARCHIVE_SHA256=$(case "$TARGETARCH" in amd64) echo "$CURSOR_ARCHIVE_SHA256_AMD64";; arm64) echo "$CURSOR_ARCHIVE_SHA256_ARM64";; *) echo "Unsupported TARGETARCH: $TARGETARCH" >&2; exit 1;; esac) && \
CURSOR_NODE_SHA256=$(case "$TARGETARCH" in amd64) echo "$CURSOR_NODE_SHA256_AMD64";; arm64) echo "$CURSOR_NODE_SHA256_ARM64";; *) echo "Unsupported TARGETARCH: $TARGETARCH" >&2; exit 1;; esac) && \
CURSOR_DIR="/home/claude/.local/share/cursor-agent/versions/$CURSOR_BUILD_ID" && \
curl -fsSL "https://downloads.cursor.com/lab/${CURSOR_BUILD_ID}/linux/${CURSOR_ASSET_ARCH}/agent-cli-package.tar.gz" -o /tmp/cursor-agent.tar.gz && \
echo "$CURSOR_ARCHIVE_SHA256 /tmp/cursor-agent.tar.gz" | sha256sum -c - && \
test "$(tar -tzf /tmp/cursor-agent.tar.gz | cut -d/ -f1 | sort -u)" = "dist-package" && \
tar -tzf /tmp/cursor-agent.tar.gz | grep -Fxq 'dist-package/cursor-agent' && \
tar -tzf /tmp/cursor-agent.tar.gz | grep -Fxq 'dist-package/node' && \
rm -rf "$CURSOR_DIR" && \
mkdir -p "$CURSOR_DIR" /home/claude/.local/bin && \
tar --strip-components=1 -xzf /tmp/cursor-agent.tar.gz -C "$CURSOR_DIR" && \
ln -sfn "$CURSOR_DIR/cursor-agent" /home/claude/.local/bin/agent && \
ln -sfn "$CURSOR_DIR/cursor-agent" /home/claude/.local/bin/cursor-agent && \
ln -sfn "$CURSOR_DIR/cursor-agent" /home/claude/.local/bin/cursor && \
echo "$CURSOR_LAUNCHER_SHA256 $CURSOR_DIR/cursor-agent" | sha256sum -c - && \
echo "$CURSOR_NODE_SHA256 $CURSOR_DIR/node" | sha256sum -c - && \
! grep -aFq -- '--permission' "$CURSOR_DIR/cursor-agent" && \
! grep -aFq -- '--allow-fs-read' "$CURSOR_DIR/cursor-agent" && \
! grep -aFq -- '--allow-fs-write' "$CURSOR_DIR/cursor-agent" && \
rm -f "$CURSOR_DIR/node" && \
ln -s /usr/local/bin/node "$CURSOR_DIR/node" && \
test "$(readlink -f "$CURSOR_DIR/node")" = "$(readlink -f /usr/local/bin/node)" && \
test "$("$CURSOR_DIR/node" --version)" = "v26.5.1" && \
test "$(cursor-agent --version)" = "$CURSOR_BUILD_ID" && \
cursor-agent --help >/dev/null && \
rm -f /tmp/cursor-agent.tar.gz
USER root
# ---------- Junie CLI (full only) ----------
USER claude
RUN if [ "$VARIANT" = "full" ]; then \
JUNIE_PLATFORM=$(case "$TARGETARCH" in amd64) echo "amd64";; arm64) echo "aarch64";; *) echo "Unsupported TARGETARCH: $TARGETARCH" >&2; exit 1;; esac) && \
JUNIE_ARCHIVE_SHA256=$(case "$TARGETARCH" in amd64) echo "$JUNIE_ARCHIVE_SHA256_AMD64";; arm64) echo "$JUNIE_ARCHIVE_SHA256_ARM64";; *) echo "Unsupported TARGETARCH: $TARGETARCH" >&2; exit 1;; esac) && \
JUNIE_ARCHIVE="junie-release-${JUNIE_VERSION}-linux-${JUNIE_PLATFORM}.zip" && \
curl -fsSL "https://github.com/jetbrains-junie/junie/releases/download/${JUNIE_VERSION}/${JUNIE_ARCHIVE}" -o "/tmp/${JUNIE_ARCHIVE}" && \
echo "$JUNIE_ARCHIVE_SHA256 /tmp/${JUNIE_ARCHIVE}" | sha256sum -c - && \
JUNIE_TARGET="/home/claude/.local/share/junie/versions/$JUNIE_VERSION" && \
JUNIE_STAGING="/home/claude/.local/share/junie/versions/.${JUNIE_VERSION}.tmp" && \
rm -rf "$JUNIE_STAGING" "$JUNIE_TARGET" && \
mkdir -p "$JUNIE_STAGING" /home/claude/.local/bin && \
JUNIE_TOP_LEVEL=$(unzip -Z1 "/tmp/${JUNIE_ARCHIVE}" | cut -d/ -f1 | sort -u | tr '\n' ' ') && \
test "$JUNIE_TOP_LEVEL" = "channel junie junie-app shim " && \
unzip -q "/tmp/${JUNIE_ARCHIVE}" 'junie-app/*' -d "$JUNIE_STAGING" && \
test -x "$JUNIE_STAGING/junie-app/bin/junie" && \
mv "$JUNIE_STAGING/junie-app" "$JUNIE_TARGET" && \
rmdir "$JUNIE_STAGING" && \
ln -sfn "$JUNIE_TARGET" /home/claude/.local/share/junie/current && \
ln -sfn /home/claude/.local/share/junie/current/bin/junie /home/claude/.local/bin/junie && \
test "$(readlink /home/claude/.local/share/junie/current)" = "/home/claude/.local/share/junie/versions/$JUNIE_VERSION" && \
/home/claude/.local/bin/junie --version >/dev/null && \
rm -f "/tmp/${JUNIE_ARCHIVE}"; \
fi
USER root
# ---------- OpenCode CLI (full only) ----------
RUN if [ "$VARIANT" = "full" ]; then \
npm i -g opencode-ai@1.18.10; \
fi
# ---------- Pi Coding Agent (full only) ----------
RUN if [ "$VARIANT" = "full" ]; then \
npm i -g --ignore-scripts @earendil-works/pi-coding-agent@0.82.1; \
fi
# Replace compatible vulnerable transitive packages without changing tool majors.
COPY scripts/patch-global-node-security-dependencies.mjs /tmp/patch-global-node-security-dependencies.mjs
RUN set -eux; \
replace_node_module() { \
package_name="$1"; \
package_version="$2"; \
archive_sha256="$3"; \
shift 3; \
archive="/tmp/${package_name}-${package_version}.tgz"; \
curl -fsSL "https://registry.npmjs.org/${package_name}/-/${package_name}-${package_version}.tgz" -o "$archive"; \
echo "$archive_sha256 $archive" | sha256sum -c -; \
test "$(tar -tzf "$archive" | cut -d/ -f1 | sort -u)" = "package"; \
for target in "$@"; do \
rm -rf "$target"; \
mkdir -p "$target"; \
tar -xzf "$archive" --strip-components=1 -C "$target"; \
test "$(node -p "require('${target}/package.json').name")" = "$package_name"; \
test "$(node -p "require('${target}/package.json').version")" = "$package_version"; \
done; \
rm -f "$archive"; \
}; \
node /tmp/patch-global-node-security-dependencies.mjs --root / --variant "$VARIANT" --check-baseline; \
replace_node_module piscina "$PISCINA_VERSION" "$PISCINA_ARCHIVE_SHA256" \
"/home/claude/.local/share/cursor-agent/versions/$CURSOR_BUILD_ID/node_modules/piscina"; \
replace_node_module brace-expansion "$BRACE_EXPANSION_VERSION" "$BRACE_EXPANSION_ARCHIVE_SHA256" \
/usr/local/lib/node_modules/npm/node_modules/brace-expansion; \
if [ "$VARIANT" = "full" ]; then \
replace_node_module brace-expansion "$BRACE_EXPANSION_VERSION" "$BRACE_EXPANSION_ARCHIVE_SHA256" \
/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/node_modules/brace-expansion; \
replace_node_module glob "$GLOB_VERSION" "$GLOB_ARCHIVE_SHA256" \
/usr/local/lib/node_modules/sharp-cli/node_modules/glob; \
replace_node_module js-yaml "$JS_YAML_VERSION" "$JS_YAML_ARCHIVE_SHA256" \
/usr/local/lib/node_modules/vercel/node_modules/js-yaml; \
replace_node_module minimatch "$MINIMATCH_5_VERSION" "$MINIMATCH_5_ARCHIVE_SHA256" \
/usr/local/lib/node_modules/eas-cli/node_modules/minimatch; \
replace_node_module minimatch "$MINIMATCH_10_VERSION" "$MINIMATCH_10_ARCHIVE_SHA256" \
/usr/local/lib/node_modules/vercel/node_modules/minimatch; \
replace_node_module node-forge "$NODE_FORGE_VERSION" "$NODE_FORGE_ARCHIVE_SHA256" \
/usr/local/lib/node_modules/eas-cli/node_modules/node-forge; \
replace_node_module path-to-regexp "$PATH_TO_REGEXP_6_VERSION" "$PATH_TO_REGEXP_6_ARCHIVE_SHA256" \
/usr/local/lib/node_modules/vercel/node_modules/@vercel/node/node_modules/path-to-regexp \
/usr/local/lib/node_modules/vercel/node_modules/@vercel/remix-builder/node_modules/path-to-regexp; \
replace_node_module path-to-regexp "$PATH_TO_REGEXP_8_VERSION" "$PATH_TO_REGEXP_8_ARCHIVE_SHA256" \
/usr/local/lib/node_modules/vercel/node_modules/path-to-regexp \
/usr/local/lib/node_modules/vercel/node_modules/@vercel/fun/node_modules/path-to-regexp; \
replace_node_module ws "$WS_VERSION" "$WS_ARCHIVE_SHA256" \
/usr/local/lib/node_modules/@cloudflare/next-on-pages/node_modules/ws; \
fi; \
node /tmp/patch-global-node-security-dependencies.mjs --root / --variant "$VARIANT"; \
test "$(npm --version)" = "11.19.0"; \
test "$(cursor-agent --version)" = "$CURSOR_BUILD_ID"; \
if [ "$VARIANT" = "full" ]; then \
eas --version >/dev/null; \
vercel --version >/dev/null; \
test "$(next-on-pages --version)" = "1.13.16"; \
sharp --version >/dev/null; \
fi; \
rm -f /tmp/patch-global-node-security-dependencies.mjs
ARG CLOUDCLI_VERSION=1.36.3
ARG CLOUDCLI_ACCOUNT_MANAGEMENT_ARTIFACT=cloudcli-ai-cloudcli-1.36.3-holyclaude-account-management.tgz
ARG CLOUDCLI_ACCOUNT_MANAGEMENT_ARTIFACT_SHA256=a90aefd34ca6ad467e911c5907fe9c19fc65e171c77145b07ea6b7a23db9bbc7
COPY vendor/artifacts/${CLOUDCLI_ACCOUNT_MANAGEMENT_ARTIFACT} /tmp/vendor/cloudcli-ai-cloudcli.tgz
COPY vendor/artifacts/cloudcli-account-management.manifest.json /tmp/vendor/cloudcli-account-management.manifest.json
COPY --chown=claude:claude vendor/locks/cloudcli-web-terminal-8aa41f614c216d961e7c0d9c3e67982c6b2d9da3.package-lock.json /tmp/vendor/web-terminal-package-lock.json
# ---------- CloudCLI (web UI for Claude Code) ----------
RUN echo "$CLOUDCLI_ACCOUNT_MANAGEMENT_ARTIFACT_SHA256 /tmp/vendor/cloudcli-ai-cloudcli.tgz" | sha256sum -c - && \
CLOUDCLI_ROOT=/usr/local/lib/node_modules/@cloudcli-ai/cloudcli && \
mkdir -p "$(dirname "$CLOUDCLI_ROOT")" /tmp/cloudcli-unpack && \
tar -xzf /tmp/vendor/cloudcli-ai-cloudcli.tgz -C /tmp/cloudcli-unpack && \
mv /tmp/cloudcli-unpack/package "$CLOUDCLI_ROOT" && \
cd "$CLOUDCLI_ROOT" && \
npm ci --omit=dev && \
node --input-type=module -e "import { createRequire } from 'node:module'; const require = createRequire('file:///usr/local/lib/node_modules/@cloudcli-ai/cloudcli/package.json'); const Database = require('better-sqlite3'); const db = new Database(':memory:'); db.exec('CREATE TABLE smoke (id INTEGER)'); db.close();" && \
chmod 0755 "$CLOUDCLI_ROOT/dist-server/server/cli.js" && \
ln -s "$CLOUDCLI_ROOT/dist-server/server/cli.js" /usr/local/bin/cloudcli && \
test -x /usr/local/bin/cloudcli && \
rm -rf /tmp/vendor/cloudcli-ai-cloudcli.tgz /tmp/cloudcli-unpack
RUN test "$(node --input-type=module -e "import { createRequire } from 'node:module'; const require = createRequire('file:///usr/local/lib/node_modules/@cloudcli-ai/cloudcli/dist-server/server/index.js'); process.stdout.write(require('playwright/package.json').version);")" = "1.61.0" && \
test -x /usr/bin/chromium
COPY scripts/patch-cloudcli-apprise-notifications.mjs /tmp/patch-cloudcli-apprise-notifications.mjs
COPY scripts/patch-cloudcli-base-path.mjs /tmp/patch-cloudcli-base-path.mjs
COPY scripts/patch-cloudcli-browser-runtime.mjs /tmp/patch-cloudcli-browser-runtime.mjs
COPY scripts/patch-cloudcli-codex-complete-exit-code.mjs /tmp/patch-cloudcli-codex-complete-exit-code.mjs
COPY scripts/patch-cloudcli-codex-permissions.mjs /tmp/patch-cloudcli-codex-permissions.mjs
COPY scripts/patch-cloudcli-disable-self-update.mjs /tmp/patch-cloudcli-disable-self-update.mjs
COPY --chown=claude:claude scripts/patch-cloudcli-web-terminal-rendering.mjs /tmp/patch-cloudcli-web-terminal-rendering.mjs
COPY scripts/verify-cloudcli-account-management-support.mjs /tmp/verify-cloudcli-account-management-support.mjs
RUN touch /usr/local/lib/node_modules/@cloudcli-ai/cloudcli/.env
# patch: launch CloudCLI Browser Use with HolyClaude's canonical Chromium
RUN node /tmp/patch-cloudcli-browser-runtime.mjs && rm -f /tmp/patch-cloudcli-browser-runtime.mjs
RUN CLOUDCLI_BROWSER_USE="/usr/local/lib/node_modules/@cloudcli-ai/cloudcli/dist-server/server/modules/browser-use/browser-use.service.js" && \
grep -Fq "// HolyClaude canonical browser runtime" "$CLOUDCLI_BROWSER_USE" && \
grep -Fq "executablePath: process.env.CHROME_PATH," "$CLOUDCLI_BROWSER_USE" && \
grep -Fq "const executablePath = process.env.CHROME_PATH || playwright.chromium.executablePath();" "$CLOUDCLI_BROWSER_USE" && \
echo "[patch] CloudCLI Browser Use canonical Chromium applied to runtime"
# patch: disable CloudCLI npm self-update inside HolyClaude (issue #50)
RUN node /tmp/patch-cloudcli-disable-self-update.mjs && rm -f /tmp/patch-cloudcli-disable-self-update.mjs
# CloudCLI 1.36.3 already contains the WebSocket binary-frame fix, provider
# model flow, and final Codex complete exit codes. Keep checks fail-closed.
RUN CLOUDCLI_WS_PROXY="/usr/local/lib/node_modules/@cloudcli-ai/cloudcli/dist-server/server/modules/websocket/services/plugin-websocket-proxy.service.js" && \
grep -q "binary: isBinary" "$CLOUDCLI_WS_PROXY" && \
echo "[patch] WebSocket frame type fix already present upstream"
RUN CLOUDCLI_COMMANDS="/usr/local/lib/node_modules/@cloudcli-ai/cloudcli/dist-server/server/routes/commands.js" && \
grep -q "providerModelsService.getProviderModels" "$CLOUDCLI_COMMANDS" && \
echo "[patch] Provider model command flow already present upstream"
RUN CLOUDCLI_CODEX="/usr/local/lib/node_modules/@cloudcli-ai/cloudcli/dist-server/server/openai-codex.js" && \
grep -q "exitCode: terminalFailure ? 1 : 0" "$CLOUDCLI_CODEX" && \
grep -q "exitCode: 1" "$CLOUDCLI_CODEX" && \
echo "[patch] Codex final completion exitCode fix already present upstream"
# patch: support serving CloudCLI below a reverse-proxy subpath (issue #64)
RUN node /tmp/patch-cloudcli-base-path.mjs && rm -f /tmp/patch-cloudcli-base-path.mjs
RUN CLOUDCLI_SERVER="/usr/local/lib/node_modules/@cloudcli-ai/cloudcli/dist-server/server/index.js" && \
CLOUDCLI_WS_SERVER="/usr/local/lib/node_modules/@cloudcli-ai/cloudcli/dist-server/server/modules/websocket/services/websocket-server.service.js" && \
grep -q "HOLYCLAUDE_BASE_PATH" "$CLOUDCLI_SERVER" && \
grep -q "sendHolyClaudeIndexHtml" "$CLOUDCLI_SERVER" && \
grep -q "stripHolyClaudeBasePathFromPathname" "$CLOUDCLI_WS_SERVER" && \
echo "[patch] CloudCLI base path support applied to runtime"
# patch: bridge Codex CloudCLI lifecycle events to Apprise (issue #17)
RUN node /tmp/patch-cloudcli-apprise-notifications.mjs && rm -f /tmp/patch-cloudcli-apprise-notifications.mjs
RUN CLOUDCLI_NOTIFICATIONS="/usr/local/lib/node_modules/@cloudcli-ai/cloudcli/dist-server/server/modules/notifications/services/notification-orchestrator.service.js" && \
test "$(grep -c "^ sendAppriseLifecycleNotification({" "$CLOUDCLI_NOTIFICATIONS")" = "2" && \
grep -q "kind: 'stop'" "$CLOUDCLI_NOTIFICATIONS" && \
grep -q "kind: 'error'" "$CLOUDCLI_NOTIFICATIONS" && \
echo "[patch] Apprise lifecycle bridge applied to CloudCLI runtime"
# patch: configure Codex CloudCLI chat permission mode (issue #18)
RUN node /tmp/patch-cloudcli-codex-permissions.mjs && rm -f /tmp/patch-cloudcli-codex-permissions.mjs
# patch: preserve explicit Codex complete fields on the 1.35.x provider path (issue #19)
RUN node /tmp/patch-cloudcli-codex-complete-exit-code.mjs && rm -f /tmp/patch-cloudcli-codex-complete-exit-code.mjs
RUN CLOUDCLI_CODEX_PROVIDER="/usr/local/lib/node_modules/@cloudcli-ai/cloudcli/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js" && \
grep -q "exitCode: 0" "$CLOUDCLI_CODEX_PROVIDER" && \
grep -q "success: true" "$CLOUDCLI_CODEX_PROVIDER" && \
grep -q "aborted: false" "$CLOUDCLI_CODEX_PROVIDER" && \
echo "[patch] Codex provider completion fields applied to CloudCLI runtime"
# patch: local account logout/password bridge for CloudCLI (issue #65)
RUN node /tmp/verify-cloudcli-account-management-support.mjs /usr/local/lib/node_modules/@cloudcli-ai/cloudcli && \
rm -f /tmp/verify-cloudcli-account-management-support.mjs /tmp/vendor/cloudcli-account-management.manifest.json
# ---------- CloudCLI plugins (baked into image) ----------
USER claude
RUN mkdir -p /home/claude/.claude-code-ui/plugins && \
git init /home/claude/.claude-code-ui/plugins/project-stats && \
cd /home/claude/.claude-code-ui/plugins/project-stats && \
git remote add origin https://github.com/cloudcli-ai/cloudcli-plugin-starter.git && \
git fetch --depth 1 origin 4895cd3fd33362471e739b786493aba048487bcc && \
git checkout --detach FETCH_HEAD && \
test "$(git rev-parse --short=12 HEAD)" = "4895cd3fd333" && \
npm ci && npm run build && \
git init /home/claude/.claude-code-ui/plugins/web-terminal && \
cd /home/claude/.claude-code-ui/plugins/web-terminal && \
git remote add origin https://github.com/cloudcli-ai/cloudcli-plugin-terminal.git && \
git fetch --depth 1 origin 8aa41f614c216d961e7c0d9c3e67982c6b2d9da3 && \
git checkout --detach FETCH_HEAD && \
test "$(git rev-parse --short=12 HEAD)" = "8aa41f614c21" && \
cp /tmp/vendor/web-terminal-package-lock.json package-lock.json && \
node /tmp/patch-cloudcli-web-terminal-rendering.mjs /home/claude/.claude-code-ui/plugins/web-terminal && \
npm ci && npm run build && \
echo '{"project-stats":{"name":"project-stats","source":"https://github.com/cloudcli-ai/cloudcli-plugin-starter","enabled":true},"web-terminal":{"name":"web-terminal","source":"https://github.com/cloudcli-ai/cloudcli-plugin-terminal","enabled":true}}' > /home/claude/.claude-code-ui/plugins.json
USER root
RUN rm -f /tmp/patch-cloudcli-web-terminal-rendering.mjs /tmp/vendor/web-terminal-package-lock.json
# ---------- Store variant for bootstrap ----------
RUN echo "${VARIANT}" > /etc/holyclaude-variant
# ---------- Copy config files ----------
COPY scripts/entrypoint.sh /usr/local/bin/entrypoint.sh
COPY scripts/bootstrap.sh /usr/local/bin/bootstrap.sh
COPY scripts/holyclaude-mosh-server /usr/local/bin/holyclaude-mosh-server
COPY scripts/persist-claude-json.mjs /usr/local/bin/persist-claude-json.mjs
COPY scripts/prepare-cli-persistence.sh /usr/local/bin/prepare-cli-persistence.sh
COPY scripts/secure-cli-persistence.py /usr/local/bin/secure-cli-persistence.py
COPY scripts/notify.py /usr/local/bin/notify.py
COPY config/settings.json /usr/local/share/holyclaude/settings.json
COPY config/claude-memory-full.md /usr/local/share/holyclaude/claude-memory-full.md
COPY config/claude-memory-slim.md /usr/local/share/holyclaude/claude-memory-slim.md
RUN chmod +x /usr/local/bin/entrypoint.sh \
/usr/local/bin/bootstrap.sh \
/usr/local/bin/holyclaude-mosh-server \
/usr/local/bin/persist-claude-json.mjs \
/usr/local/bin/prepare-cli-persistence.sh \
/usr/local/bin/secure-cli-persistence.py \
/usr/local/bin/notify.py
RUN mkdir -p /usr/local/lib/holyclaude && \
if [ -x /usr/bin/mosh-server ]; then \
mv /usr/bin/mosh-server /usr/local/lib/holyclaude/mosh-server.real && \
ln -sf /usr/local/bin/holyclaude-mosh-server /usr/bin/mosh-server; \
fi
# ---------- s6-overlay service definitions ----------
COPY s6-overlay/s6-rc.d/cloudcli/type /etc/s6-overlay/s6-rc.d/cloudcli/type
COPY s6-overlay/s6-rc.d/cloudcli/run /etc/s6-overlay/s6-rc.d/cloudcli/run
COPY s6-overlay/s6-rc.d/persist-claude-json/type /etc/s6-overlay/s6-rc.d/persist-claude-json/type
COPY s6-overlay/s6-rc.d/persist-claude-json/run /etc/s6-overlay/s6-rc.d/persist-claude-json/run
COPY s6-overlay/s6-rc.d/xvfb/type /etc/s6-overlay/s6-rc.d/xvfb/type
COPY s6-overlay/s6-rc.d/xvfb/run /etc/s6-overlay/s6-rc.d/xvfb/run
COPY s6-overlay/s6-rc.d/sshd/type /etc/s6-overlay/s6-rc.d/sshd/type
COPY s6-overlay/s6-rc.d/sshd/run /etc/s6-overlay/s6-rc.d/sshd/run
RUN chmod +x /etc/s6-overlay/s6-rc.d/cloudcli/run \
/etc/s6-overlay/s6-rc.d/persist-claude-json/run \
/etc/s6-overlay/s6-rc.d/xvfb/run \
/etc/s6-overlay/s6-rc.d/sshd/run && \
touch /etc/s6-overlay/user-bundles.d/user/contents.d/cloudcli && \
touch /etc/s6-overlay/user-bundles.d/user/contents.d/persist-claude-json && \
touch /etc/s6-overlay/user-bundles.d/user/contents.d/xvfb
# ---------- Working directory ----------
WORKDIR /workspace
# ---------- Health check ----------
HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 \
CMD curl -sf http://localhost:3001/ || exit 1
# ---------- s6-overlay as PID 1 ----------
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]