Files
chejiulong 743ae80d71 fix(deploy): ship data/ so Docker images keep their baked plugin previews (#7028)
`deploy/Dockerfile` copies skills, design-systems, craft, prompt-templates,
assets and plugins/_official, but not data/ — so
`data/plugin-previews/manifest.json`, which CI commits precisely so deployments
can find the bakes, is absent at runtime. `resolvePluginPreviewsDir()` defaults
to <projectRoot>/data/plugin-previews, `loadManifest()` returns {},
`applyBakedPreviews()` returns the records untouched, and
`inferPluginPreview(record, { preferBaked: true })` falls through to the live
iframe for every plugin.

Measured on 0.19.2 with 461 registered plugins: the checked-in manifest has 176
entries and 0 records carried od.bakedPreview. With data/ present the same
daemon reports 176 — deck 79, prototype 79, video 10, scenario 4, image 1,
audio 1, template 2. The packaged app reaches the manifest through its own
packaging path, so only Docker/self-hosted deployments are affected.

Also warn once per path when the manifest is absent. `loadManifest()` already
carries the reasoning for the malformed case — «would otherwise silently
disable every baked preview with no trace; surface it so it's diagnosable» —
but the absent branch returned {} with no message, which is the same failure
with the same lack of trace.

Fixes #7018

Co-authored-by: Jason <>
2026-09-01 09:03:36 +00:00

122 lines
4.4 KiB
Docker

ARG NODE_IMAGE=docker.io/library/node:24-alpine
ARG RUNTIME_IMAGE=docker.io/library/node:24-alpine
FROM ${NODE_IMAGE} AS build
ARG HTTP_PROXY
ARG HTTPS_PROXY
ARG http_proxy
ARG https_proxy
ARG no_proxy
ARG NO_PROXY
ENV HTTP_PROXY=${HTTP_PROXY}
ENV HTTPS_PROXY=${HTTPS_PROXY}
ENV http_proxy=${http_proxy}
ENV https_proxy=${https_proxy}
ENV no_proxy=${no_proxy}
ENV NO_PROXY=${NO_PROXY}
ENV CI=true
RUN apk add --no-cache python3 make g++
WORKDIR /app
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
COPY scripts/postinstall.mjs ./scripts/postinstall.mjs
COPY packages ./packages
COPY tools ./tools
COPY apps/daemon/package.json ./apps/daemon/package.json
COPY apps/web/package.json ./apps/web/package.json
COPY e2e/package.json ./e2e/package.json
RUN corepack enable && \
corepack prepare pnpm@10.33.2 --activate && \
pnpm install --frozen-lockfile
COPY apps ./apps
RUN pnpm --filter @open-design/daemon build && \
pnpm --filter @open-design/web build && \
pnpm --filter @open-design/daemon deploy --legacy --prod /app/deploy/daemon && \
pnpm store prune && \
rm -rf \
/root/.cache \
/root/.local/share/pnpm/store \
/app/deploy/daemon/node_modules/.cache \
/app/deploy/daemon/node_modules/@types \
/app/deploy/daemon/node_modules/.pnpm/@types+* \
/app/deploy/daemon/node_modules/.pnpm/better-sqlite3@*/node_modules/better-sqlite3/deps \
/app/deploy/daemon/node_modules/.pnpm/better-sqlite3@*/node_modules/better-sqlite3/src && \
find /app/deploy/daemon/node_modules -type d \( \
-name test -o \
-name tests -o \
-name "__tests__" -o \
-name docs -o \
-name doc -o \
-name example -o \
-name examples -o \
-name ".github" \
\) -prune -exec rm -rf '{}' + && \
find /app/deploy/daemon/node_modules -type f \( \
-name "*.md" -o \
-name "*.markdown" -o \
-name "*.d.ts" -o \
-name "*.d.cts" -o \
-name "*.d.mts" -o \
-name "*.map" -o \
-name "*.tsbuildinfo" -o \
-name "binding.gyp" \
\) -delete
# Stage-2 assets — copied into the build stage so the runtime stage
# can pull them via --from=build instead of reaching back into the
# build context. This lets the Dockerfile work when the build context
# differs from the repo root (Railway, CI runners with a subdirectory
# context, etc.).
COPY skills ./skills
COPY design-systems ./design-systems
COPY craft ./craft
COPY prompt-templates ./prompt-templates
COPY assets ./assets
# `data/plugin-previews/manifest.json` is committed by the bake pipeline and is
# what `resolvePluginPreviewsDir()` looks for at runtime. Without it in the
# image, `loadManifest()` returns `{}` and every plugin silently falls back to
# the live-iframe preview path — the exact cost the bakes exist to avoid.
COPY data ./data
COPY plugins/_official ./plugins/_official
FROM ${RUNTIME_IMAGE}
RUN apk add --no-cache tini poppler-utils bash git && \
addgroup -S -g 1001 open-design && \
adduser -S -D -H -u 1001 -G open-design open-design
WORKDIR /app
COPY --from=build --chown=open-design:open-design /app/deploy/daemon ./apps/daemon
COPY --from=build --chown=open-design:open-design /app/apps/web/out ./apps/web/out
COPY --from=build --chown=open-design:open-design /app/skills ./skills
COPY --from=build --chown=open-design:open-design /app/design-systems ./design-systems
COPY --from=build --chown=open-design:open-design /app/craft ./craft
COPY --from=build --chown=open-design:open-design /app/prompt-templates ./prompt-templates
COPY --from=build --chown=open-design:open-design /app/data ./data
COPY --from=build --chown=open-design:open-design /app/assets/frames ./assets/frames
COPY --from=build --chown=open-design:open-design /app/assets/community-pets ./assets/community-pets
# Plan §3.J4 / spec §23.3.5 — bundled atom plugins registered on
# daemon boot. The directory contains `plugins/_official/atoms/<atom>/`
# pairs (SKILL.md + open-design.json); registerBundledPlugins() walks
# it on startup so the container ships with first-party atoms reachable
# via the same registry path third-party plugins use.
COPY --from=build --chown=open-design:open-design /app/plugins/_official ./plugins/_official
RUN mkdir -p /app/.od && \
chown -R open-design:open-design /app
ENV NODE_ENV=production
ENV NODE_OPTIONS=--max-old-space-size=192
ENV OD_BIND_HOST=0.0.0.0
ENV OD_PORT=7456
EXPOSE 7456
USER open-design
ENTRYPOINT ["/sbin/tini", "--"]
CMD ["node", "apps/daemon/dist/cli.js", "--no-open"]