Repository navigation
Expand file tree
/
Copy pathDockerfile
More file actions
188 lines (158 loc) · 10.1 KB
/
Copy pathDockerfile
File metadata and controls
188 lines (158 loc) · 10.1 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
# No `# syntax=` directive: BuildKit then uses its built-in dockerfile
# frontend, so no frontend image is pulled from Docker Hub at build start —
# the only syntax-gated feature used below is RUN --mount (frontend 1.2+),
# far older than any bundled frontend.
#
# Base images come from the org's GHCR mirror (`mirror-*`), refreshed from
# the upstream library by .github/workflows/mirror-images.yml — Docker Hub
# and ECR Public both rate-limit shared CI egress IPs (Docker Hub 504s
# motivated the ECR move; ECR then throttled with `toomanyrequests`). The
# mirror copies upstream manifests verbatim, so the pinned digests are the
# upstream ones.
FROM ghcr.io/nearbuilders/mirror-node:24-alpine@sha256:6a1c190e17215d844926b0677f4bf87273ff7d5e54db06d3b5b5863ab0b09a3d AS builder
WORKDIR /app
# pnpm 12 resolves git-hosted dependencies by shelling out to git — the
# catalog pins one (@hot-labs/near-connect from the near-connect fork), and
# the pre-run installs verifyDepsBeforeRun triggers for `pnpm run` below
# re-resolve it. The alpine node base ships without git.
RUN apk add --no-cache git
# The git SHA the build's bytes bake into the platform release manifest
# (#537): docker contexts carry no .git, so the workflow / local deploy
# train pass it as a build arg; omitted (regression builds) → the field
# drops from the manifest instead of inventing a value. ARG stays a
# Dockerfile-stub until this ENV brings it into the script's environment.
ARG BOS_GIT_SHA
# pnpm comes from Corepack — the root package.json `packageManager` field is
# the single version source (ADR 0026 §5: the universal image is node-based
# with corepack pnpm). The download prompt is off because image builds have
# no TTY; the warm-up run resolves the pinned pnpm into Corepack's cache in
# its own layer so `COPY . .` busts never re-download it.
ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0
COPY package.json ./
RUN corepack enable && pnpm --version
# NOTE (ADR 0026): pnpm settings live in pnpm-workspace.yaml (ignoreScripts,
# nodeLinker=hoisted, linkWorkspacePackages) — no .npmrc is required.
# Do NOT split this into a manifests-first COPY + install: pnpm's frozen
# install on a manifests-only context is unverified, and the workspace bin
# links (the check below) depend on the workspace sources being present.
# Full source + a store cache mount keeps re-downloads free when the layer
# busts.
COPY . .
RUN --mount=type=cache,id=pnpm-store,target=/root/.local/share/pnpm/store \
pnpm install --frozen-lockfile --ignore-scripts
RUN test -e node_modules/.bin/every-plugin \
|| { echo "workspace bin not linked — workspace sources must be present at install time"; exit 1; }
RUN pnpm --filter every-plugin build
RUN pnpm --filter better-near-auth build
RUN pnpm --filter everything-dev build
# The platform release manifest is generated in-build (ADR 0034): one
# composer, two homes — the deploy train publishes the same bytes to the
# FastKV pointer slot, the image stages them for self-description
# (`bos platform manifest` reads it from the known path).
ENV BOS_GIT_SHA=$BOS_GIT_SHA
RUN node --import tsx scripts/generate-platform-manifest.ts
RUN node --import tsx scripts/resolve-workspace-refs.ts
# ── Dist build (ADR 0009): all workspaces the start stack serves ──
# Forked BEFORE the prod stage strips sources — the ui/api/plugins builds run here.
# DEPLOY=true selects dist-first resolution and drops source maps — the same
# mode the deploy train's CDN uploads use, so image bytes ≡ CDN bytes.
FROM builder AS dist-builder
ENV DEPLOY=true
RUN node --import tsx scripts/regression/container-build.ts
# ── Prod build: strip sources — the framework loads remotes at runtime ──
# The authored descriptor (bos.app.ts / bos.dev.ts) goes too: the image
# carries no config at all — boot is identity-driven (BOS_ACCOUNT/
# BOS_GATEWAY env → FastKV) or a mounted config — and findConfigPath must
# not shadow either with a half-stripped authored form.
FROM builder AS prod-builder
# Derived node_modules prune: keep the union of every workspace's production
# dependency closure, delete the rest. Runs BEFORE the source strip — the
# seeds are the workspace package.jsons. nodeLinker=hoisted (ADR 0026 §4)
# keeps this a flat tree the prune walker understands.
RUN node --import tsx scripts/prune-runtime-node-modules.ts /app
RUN rm -rf host api ui plugins && rm -f bos.app.ts bos.dev.ts pnpm-workspace.yaml
# Framework package copies carry only their runtime surface: dist + manifest
# (banner/version reads). src/, skills/, and dev metadata never load in the
# start stack — the image resolves dist under every condition (ADR 0018).
RUN rm -rf packages/everything-dev/src packages/everything-dev/skills \
packages/everything-dev/CHANGELOG.md packages/everything-dev/README.md \
packages/everything-dev/tsconfig*.json packages/everything-dev/vitest.config.ts \
packages/every-plugin/src packages/every-plugin/skills \
packages/every-plugin/CHANGELOG.md packages/every-plugin/README.md \
packages/every-plugin/tsconfig*.json packages/every-plugin/vitest*.ts \
packages/every-plugin/bin \
packages/better-near-auth/src packages/better-near-auth/skills \
packages/better-near-auth/CHANGELOG.md packages/better-near-auth/README.md \
packages/better-near-auth/tsconfig.json packages/better-near-auth/vitest.config.ts
# Clean broken workspace symlinks (the stripped workspaces leave dead links)
RUN find node_modules -maxdepth 1 -type l ! -exec test -e {} \; -print -delete 2>/dev/null || true
# ── Shared runtime base (ADR 0021): hardened user + env both final stages
# inherit. The probe uses node's built-in fetch — no curl in the runtime
# layers.
FROM ghcr.io/nearbuilders/mirror-node:24-alpine@sha256:6a1c190e17215d844926b0677f4bf87273ff7d5e54db06d3b5b5863ab0b09a3d AS runtime-base
WORKDIR /app
RUN addgroup -g 1001 -S appgroup && adduser -S appuser -u 1001
ENV NODE_ENV=production
ENV HOST=0.0.0.0
# ── Regression fixture (ADR 0009): the browser-suite harness image ──
# Serves the staged dists on container-local static servers and boots the
# production host over a baked local config (--config-path). Built explicitly
# with `docker build --target regression` — not the deployment artifact.
FROM runtime-base AS regression
ENV PORT=4100
EXPOSE 4100
HEALTHCHECK --interval=10s --timeout=3s --start-period=180s --retries=5 \
CMD node -e "fetch('http://127.0.0.1:'+(process.env.PORT||4100)+'/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"
COPY --from=dist-builder --chown=appuser:appgroup /app/node_modules ./node_modules
COPY --from=dist-builder --chown=appuser:appgroup /app/package.json .
# Generated config (ADR 0005) — baked as the bare-start boot fallback.
COPY --from=dist-builder --chown=appuser:appgroup /app/.bos/bos.resolved-config.json ./bos.config.json
COPY --from=dist-builder --chown=appuser:appgroup /app/packages/everything-dev ./packages/everything-dev
COPY --from=dist-builder --chown=appuser:appgroup /app/packages/every-plugin ./packages/every-plugin
COPY --from=dist-builder --chown=appuser:appgroup /app/packages/better-near-auth ./packages/better-near-auth
COPY --from=dist-builder --chown=appuser:appgroup /app/scripts/regression ./scripts/regression
COPY --from=dist-builder --chown=appuser:appgroup /app/.bos/regression/image ./.bos/regression/image
COPY --from=dist-builder --chown=appuser:appgroup /app/.bos/bundles ./.bos/bundles
ENV BOS_BUNDLE_DIR=/app/.bos/bundles
RUN mkdir -p .bos/generated .bos/logs && \
chown -R appuser:appgroup .bos && \
chown appuser:appgroup /app
USER appuser
ENTRYPOINT ["node", "scripts/regression/container-entrypoint.mjs"]
# ── Deployment runtime (ADR 0021): the last stage — the universal image ──
# `node …/cli.mjs start` boots `bos start`: identity-driven (BOS_ACCOUNT/
# BOS_GATEWAY env → FastKV) or from a mounted config (--config-path, or a
# bos.config.json mounted at /app). Distribution lives on the CDN (ADR 0020)
# and the image stages no dists and no cache: every boot fetches the
# published bytes over the network under a bounded retry policy, and a CDN
# outage fails loud instead of serving pre-reset bytes from a stale-if-error
# disk cache. The start stack loads the host, api, and plugins in-process
# through Module Federation — no child spawns, so the runtime needs only
# node. This is the default build target; GHCR publishes it. One container =
# the whole start stack; only the host port is mapped. Databases and secrets
# arrive via env at `docker run`.
FROM runtime-base AS runtime
COPY --from=prod-builder --chown=appuser:appgroup /app/node_modules ./node_modules
COPY --from=prod-builder --chown=appuser:appgroup /app/package.json .
COPY --from=prod-builder --chown=appuser:appgroup /app/packages/everything-dev ./packages/everything-dev
COPY --from=prod-builder --chown=appuser:appgroup /app/packages/every-plugin ./packages/every-plugin
COPY --from=prod-builder --chown=appuser:appgroup /app/packages/better-near-auth ./packages/better-near-auth
# Boot assert: the CLI dist's runtime imports must resolve inside the image —
# a missing workspace (the better-near-auth dangling-symlink class) fails the
# image build here instead of the post-publish smoke + config rollback.
RUN node --input-type=module -e \
"await import('/app/packages/everything-dev/dist/services/auth/device-login.mjs')"
# No BOS_BUNDLE_DIR (CDN-only) and no BOS_BUNDLE_CACHE_DIR (a self-activating
# cache could serve older bundles after a reset or rollback); both remain
# operator opt-ins via `docker run -e`.
RUN mkdir -p .bos/generated .bos/logs && \
chown -R appuser:appgroup .bos && \
chown appuser:appgroup /app
ENV PORT=3000
# BOS_ENV: set to "staging" to enable staging mode (uses staging domain for BOS_GATEWAY)
# Defaults to "production" if unset.
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=3s --start-period=120s --retries=3 \
CMD node -e "fetch('http://127.0.0.1:'+(process.env.PORT||3000)+'/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"
USER appuser
CMD ["sh", "-c", "node ./node_modules/everything-dev/dist/cli.mjs start --no-interactive --port ${PORT:-3000}"]