-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathDockerfile
More file actions
293 lines (256 loc) · 16.2 KB
/
Copy pathDockerfile
File metadata and controls
293 lines (256 loc) · 16.2 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
# syntax=docker/dockerfile:1
# ═══════════════════════════════════════════════════════════════════════════
# LOCALPIBOX DEVSTACK — MULTI-STAGE DOCKERFILE
# ═══════════════════════════════════════════════════════════════════════════
# Two build targets:
# cli — Base dev environment with Pi CLI (interactive terminal)
# web — Extends cli + adds VSCodium server + web access
#
# Build:
# docker build --target cli -t ghcr.io/lpb-stack/devstack:cli .
# docker build --target web -t ghcr.io/lpb-stack/devstack:web .
#
# Fork configuration: lpb.stack.env (project root)
# Sourced at build time to populate ARG defaults:
# LPB_PI_FORK, LPB_PI_REF, LPB_NODE_VERSION, LPB_VSCODIUM_VERSION
#
# NOTE: Runtime extensions (lemonade-pi-plugin, lpb-memory) are NOT
# built into the image. They are installed at container startup by
# `pi update --extensions`, which reads their branches from the
# config repo served at ~/.pi/agent/settings.json → "packages" array.
# ═══════════════════════════════════════════════════════════════════════════
# Source fork configuration — ARG defaults come from lpb.stack.env
# Instead of editing this file, edit lpb.stack.env (canonical) and either:
# - run support/build.sh (reads lpb.stack.env + lpb.conf.env), or
# - pass --build-arg per value.
# NOTE: Docker ARG values can't reference source'd shell variables.
ARG NODE_VERSION=24
ARG VSCODIUM_VERSION=1.126.04524
ARG PI_FORK=https://github.com/lpb-stack/pi.git
ARG PI_REF=lpb
ARG PI_HEAD_SHA=unknown
# Config preset repo — baked so start.sh clones the fork's config at boot.
# (CI passes these from lpb.stack.env LPB_CONFIG_FORK / LPB_CONFIG_REF.)
ARG CONFIG_FORK=https://github.com/lpb-stack/config.git
ARG CONFIG_REF=main
# Runtime defaults baked from lpb.conf.env (start.sh can still override).
ARG LPB_MAX_TOKENS_CONTEXT_RATIO=0.06
ARG LPB_VERSION=unknown
# Provenance — set via --build-arg in CI, defaults to "unknown" for local builds
ARG IMAGE_REVISION=unknown
ARG IMAGE_BUILT=unknown
# ═══════════════════════════════════════════════════════════════════════════
# BASE STAGE — Common setup for both cli and web images
# ═══════════════════════════════════════════════════════════════════════════
FROM ubuntu:26.04 AS base
ARG NODE_VERSION
ARG VSCODIUM_VERSION
ARG PI_FORK
ARG PI_REF
ARG PI_HEAD_SHA
ARG CONFIG_FORK
ARG CONFIG_REF
ARG LPB_MAX_TOKENS_CONTEXT_RATIO
ARG LPB_VERSION
ARG IMAGE_REVISION
ARG IMAGE_BUILT
ENV DEBIAN_FRONTEND=noninteractive
# Context window / max tokens ratio — baked into image from lpb.conf.env.
# Container-level overrides take precedence via --env or -e at runtime.
ENV LPB_MAX_TOKENS_CONTEXT_RATIO=${LPB_MAX_TOKENS_CONTEXT_RATIO}
# Stack/version banner value (baked from lpb.conf.env).
ENV LPB_VERSION=${LPB_VERSION}
# Baked from lpb.stack.env/CI — start.sh reads these for the config repo clone.
ENV LPB_CONFIG_REMOTE=${CONFIG_FORK}
ENV LPB_CONFIG_REF=${CONFIG_REF}
# ── System packages ─────────────────────────────────────────────────────────
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential pkg-config \
curl ca-certificates gnupg \
git jq unzip rsync \
python3 python3-pip python3-venv libsqlite3-dev \
sudo openssh-server openssl \
gh ripgrep fzf fd-find tmux \
&& rm -rf /var/lib/apt/lists/*
# ── Node.js ─────────────────────────────────────────────────────────────────
RUN curl -fsSL https://deb.nodesource.com/setup_${NODE_VERSION}.x | bash - \
&& apt-get install -y nodejs \
&& npm install -g npm@latest \
&& corepack enable
# ── User lpb (UID 1000) ────────────────────────────────────────────────────
RUN set -eux; \
if getent passwd 1000 >/dev/null; then \
oldname="$(getent passwd 1000 | cut -d: -f1)"; \
if [ "$oldname" != "lpb" ]; then \
usermod -l lpb -d /home/lpb -m "$oldname"; \
if getent group 1000 >/dev/null; then \
oldgroup="$(getent group 1000 | cut -d: -f1)"; \
[ "$oldgroup" = "lpb" ] || groupmod -n lpb "$oldgroup"; \
fi; \
fi; \
else \
useradd -m -s /bin/bash -u 1000 lpb; \
fi; \
chown -R 1000:1000 /home/lpb
RUN echo '%sudo ALL=(ALL) NOPASSWD:ALL' > /etc/sudoers.d/nopasswd && chmod 440 /etc/sudoers.d/nopasswd
# ── Global npm packages ─────────────────────────────────────────────────────
# Cache mounted — persists downloaded tarballs across builds.
RUN --mount=type=cache,target=/home/lpb/.npm \
set -eux; \
npm config set prefix '/home/lpb/.npm-global'; \
npm config set fetch-retries 5; \
npm config set fetch-retry-mintimeout 20000; \
npm config set fetch-retry-maxtimeout 120000; \
npm config set fetch-timeout 300000; \
npm config set registry https://registry.npmjs.org/; \
printf 'allow-scripts=better-sqlite3\nallow-scripts=agent-browser\nallow-scripts=esbuild\nallow-scripts=protobufjs\nallow-scripts=@google/genai\n' > /home/lpb/.npmrc; \
npm install -g zod@3 agent-browser exa-mcp-server; \
chown -R 1000:1000 /home/lpb/.npm-global
# ── Pi monorepo build ───────────────────────────────────────────────────────
RUN set -eux; \
export PATH="/home/lpb/.npm-global/bin:${PATH}"; \
mkdir -p /opt/pi-src && cd /opt/pi-src; \
git clone --branch ${PI_REF} ${PI_FORK} .; \
git fetch origin ${PI_REF}; \
git checkout ${PI_REF}; \
git config user.email "build@lpb-stack.dev"; \
git config user.name "LocalPibox Build"; \
npm install; \
ln -sf "$(pwd)/node_modules/@typescript/native-preview-linux-x64/lib/tsgo" "$(pwd)/node_modules/.bin/tsgo"; \
export PATH="$(pwd)/node_modules/.bin:${PATH}"; \
npm run build; \
mkdir -p /tmp/pi-packs; \
for pkg in ai agent coding-agent tui; do \
npm pack "./packages/$pkg" --pack-destination /tmp/pi-packs; \
done; \
if [ -d "./packages/client" ]; then \
npm pack "./packages/client" --pack-destination /tmp/pi-packs; \
fi; \
npm install -g /tmp/pi-packs/*.tgz; \
rm -rf /tmp/pi-packs; \
/home/lpb/.npm-global/bin/pi --version || (echo "FATAL: pi binary not functional" && exit 1); \
grep -rq 'Case 4' /home/lpb/.npm-global/lib/node_modules/@earendil-works/ \
|| (echo "FATAL: Case 4 patch missing from pi-ai" && exit 1); \
grep -rq 'qwen-chat-template' /home/lpb/.npm-global/lib/node_modules/@earendil-works/pi-ai/dist/ \
|| (echo "FATAL: Qwen reasoning_effort patch missing" && exit 1); \
grep -rq 'allowScripts' /home/lpb/.npm-global/lib/node_modules/@earendil-works/pi-coding-agent/package.json \
|| (echo "FATAL: pi-coding-agent allowScripts missing (regression in pi fork)" && exit 1); \
ls -la /home/lpb/.npm-global/bin/; \
rm -rf /opt/pi-src/.git /opt/pi-src/src /opt/pi-src/test /opt/pi-src/tests
# ── Switch to non-root user ─────────────────────────────────────────────────
USER lpb
# ── Extensions + Config ─────────────────────────────────────────────────────
# Config repo is cloned at container start by start.sh — the image stays lean.
# ═══════════════════════════════════════════════════════════════════════════
# CLI IMAGE
# ═══════════════════════════════════════════════════════════════════════════
FROM base AS cli
# ── Support utilities (moved from config/support/ to devstack/support/) ──
# Copied to /opt/pi-support/ — used by start.sh at runtime
COPY --chmod=755 support/browser-state-cleanup.py /opt/pi-support/browser-state-cleanup.py
COPY support/browser-validate.ts /opt/pi-support/browser-validate.ts
COPY --chmod=755 support/_lib.sh /opt/pi-support/_lib.sh
COPY support/session-uuid.ts /opt/pi-support/session-uuid.ts
COPY --chmod=755 support/browser /opt/pi-support/browser
COPY support/validate-subagent-output.ts /opt/pi-support/validate-subagent-output.ts
COPY support/config/ /opt/pi-support/config/
COPY support/docs/ /opt/pi-support/docs/
COPY support/schemas/ /opt/pi-support/schemas/
# Shared Python helpers used by the ported support tools (import via the
# script directory, which Python adds to sys.path automatically).
COPY scripts/localpibox/ /opt/pi-support/lpb-stack/
# ── Devstack deployment scripts ──
COPY --chmod=755 support/install-browser.py /opt/devstack/install-browser.py
COPY --chmod=755 support/validate.py /opt/devstack/validate.py
COPY --chmod=755 support/install-openspec.py /opt/pi-support/install-openspec.py
COPY --chmod=755 support/start.sh /opt/devstack/start.sh
COPY lpb.conf.env /opt/devstack/lpb.conf.env
COPY lpb.stack.env /opt/devstack/lpb.stack.env
COPY --chmod=755 support/entrypoint-cli.sh /opt/devstack/entrypoint-cli.sh
# ── User-facing support utilities (baked, not first-run) ────────────────
# These are immutable tools users invoke from PATH; they live in the image
# layer so they survive rebuilds/recreates. Symlinks are created here (as
# lpb) rather than at container first-run — ~/.local/bin is not a mount, so
# a first-run-only ln would vanish on the next rebuild.
RUN mkdir -p /home/lpb/.local/bin \
&& ln -sf /opt/devstack/install-browser.py /home/lpb/.local/bin/install-browser \
&& ln -sf /opt/devstack/validate.py /home/lpb/.local/bin/validate \
&& ln -sf /opt/pi-support/install-openspec.py /home/lpb/.local/bin/install-openspec \
&& ln -sf /opt/pi-support/browser-state-cleanup.py /home/lpb/.local/bin/browser-state-cleanup \
&& ln -sf /opt/pi-support/lpb-config.py /home/lpb/.local/bin/lpb-config
# ── Shell PATH helper ───────────────────────────────────────────────────────
# lpb-config.py needs to live under /opt/pi-support/ for its sys.path resolution.
# A symlink at ~/.local/bin/lpb-config gives users the clean CLI name.
COPY --chmod=755 support/lpb-config.py /opt/pi-support/lpb-config.py
# ── Root operations: ownership + gitconfig + shell PATH ─────────────────────
USER root
RUN mkdir -p /home/lpb/.agent-browser/sessions \
&& chown -R 1000:1000 /home/lpb /opt/devstack /opt/pi-support \
&& printf '[credential "https://github.com"]\n helper = !gh auth git-credential\n[credential "https://gist.github.com"]\n helper = !gh auth git-credential\n' > /home/lpb/.gitconfig \
&& chown 1000:1000 /home/lpb/.gitconfig \
&& printf '\n# LocalPibox: npm-global and local bin on PATH\nexport PATH="/home/lpb/.npm-global/bin:/home/lpb/.local/bin:${PATH}"\n' >> /home/lpb/.bashrc
USER lpb
WORKDIR /home/lpb/workspace
LABEL org.opencontainers.image.title="LocalPibox Devstack — CLI" \
org.opencontainers.image.description="AI-powered dev environment with Pi CLI (interactive terminal)" \
org.opencontainers.image.source="https://github.com/lpb-stack/devstack" \
org.opencontainers.image.vendor="LocalPibox" \
org.opencontainers.image.licenses="MIT" \
org.opencontainers.image.revision="${IMAGE_REVISION}" \
org.opencontainers.image.created="${IMAGE_BUILT}"
ENTRYPOINT ["/opt/devstack/entrypoint-cli.sh"]
# ═══════════════════════════════════════════════════════════════════════════
# WEB IMAGE — Extends cli + adds VSCodium + web access
# ═══════════════════════════════════════════════════════════════════════════
FROM cli AS web
# ── VSCodium ────────────────────────────────────────────────────────────────
USER root
RUN curl -fsSL \
"https://github.com/VSCodium/vscodium/releases/download/${VSCODIUM_VERSION}/vscodium-reh-web-linux-x64-${VSCODIUM_VERSION}.tar.gz" \
-o /tmp/vscodium.tar.gz \
&& mkdir -p /opt/vscodium \
&& tar -xzf /tmp/vscodium.tar.gz -C /opt/vscodium --strip-components=1 \
&& rm /tmp/vscodium.tar.gz
# ── VSCodium extensions ─────────────────────────────────────────────────────
RUN set -eux; \
export PATH="/home/lpb/.npm-global/bin:${PATH}"; \
EXT_DIR="/home/lpb/.vscodium-server/extensions"; \
mkdir -p "${EXT_DIR}"; \
install_ext() { \
publisher="$1"; name="$2"; \
meta_url="https://open-vsx.org/api/${publisher}/${name}"; \
version=$(curl -fsSL "${meta_url}" | jq -r '.version'); \
if [ -z "$version" ] || [ "$version" = "null" ]; then echo "WARN: no version for ${publisher}.${name}"; return 0; fi; \
vsix_url=$(curl -fsSL "${meta_url}" | jq -r '.files.download'); \
if [ -z "$vsix_url" ] || [ "$vsix_url" = "null" ]; then echo "WARN: no download for ${publisher}.${name}"; return 0; fi; \
dest="${EXT_DIR}/${publisher}.${name}-${version}"; \
mkdir -p "${dest}"; \
curl -fsSL "${vsix_url}" -o /tmp/ext.vsix || return 0; \
rm -rf /tmp/ext_extracted; mkdir -p /tmp/ext_extracted; \
unzip -q /tmp/ext.vsix -d /tmp/ext_extracted; \
cp -r /tmp/ext_extracted/extension/. "${dest}/"; \
rm -rf /tmp/ext.vsix /tmp/ext_extracted; \
echo " Installed ${publisher}.${name}@${version}"; \
}; \
install_ext pi0 pi-vscode || echo "WARN: pi-vscode install failed"; \
chown -R 1000:1000 /home/lpb/.vscodium-server
COPY --chmod=755 support/entrypoint-web.sh /opt/devstack/entrypoint-web.sh
RUN chown -R 1000:1000 /home/lpb
USER lpb
WORKDIR /home/lpb/workspace
ENV ED_PORT=3000
ENV HOST=0.0.0.0
# Auto-generated random token on container start.
# Override with --env CONNECTION_TOKEN=xxx or set a fixed value for persistence.
ENV CONNECTION_TOKEN=
# Healthcheck uses bare names (VSCodium server reads them)
HEALTHCHECK --interval=30s --timeout=5s --retries=3 --start-period=60s \
CMD curl -sf "http://localhost:${ED_PORT}/" >/dev/null 2>&1 && curl -sf "http://localhost:${ED_PORT}/?tkn=${CONNECTION_TOKEN:-}" >/dev/null 2>&1 || exit 1
LABEL org.opencontainers.image.title="LocalPibox Devstack — Web" \
org.opencontainers.image.description="AI-powered dev environment with VSCodium web editor" \
org.opencontainers.image.source="https://github.com/lpb-stack/devstack" \
org.opencontainers.image.vendor="LocalPibox" \
org.opencontainers.image.licenses="MIT" \
org.opencontainers.image.revision="${IMAGE_REVISION}" \
org.opencontainers.image.created="${IMAGE_BUILT}"
ENTRYPOINT ["/opt/devstack/entrypoint-web.sh"]