Repository navigation
Expand file tree
/
Copy pathDockerfile
More file actions
98 lines (94 loc) · 5.37 KB
/
Copy pathDockerfile
File metadata and controls
98 lines (94 loc) · 5.37 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
# Single-file build. Two targets:
# docker build --target runtime -t cometchat-mcp:base . # hermetic, no docs clone
# docker build --target full --build-arg DOCS_COMMIT=<sha> -t <img>:<tag> .
#
# Works on BuildKit (Docker 23+ default) and on the legacy builder alike:
# `runtime` is declared BEFORE `indexer`, and both builders stop at --target,
# so --target runtime never executes the docs clone on either one.
#
# NOTE: a bare `docker build .` with no --target now builds `full` (the last
# stage) and will FAIL on the DOCS_COMMIT guard below. That is deliberate —
# but it means every call site must pass --target explicitly.
# Node 24 (LTS) is the node-version CI tests on (.github/workflows/ci.yml);
# keep the two in step. Node 20 reached end of life in April 2026.
FROM node:24.22.1-bookworm-slim AS build
# Toolchain for native-module source builds (better-sqlite3 has no prebuilt
# binaries on some architectures, e.g. linux/arm64).
RUN apt-get update -qq && apt-get install -y -qq --no-install-recommends python3 make g++ >/dev/null \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY package.json package-lock.json tsconfig.json ./
RUN npm ci --no-audit --no-fund
COPY src ./src
COPY scripts ./scripts
COPY skills ./skills
RUN npm run build
# The runtime stage copies this node_modules, so drop devDependencies now that
# tsc has run. tsx stays: it is a production dependency because the refresher
# and the indexer stage run scripts/build-index.ts under it. Prune only deletes
# packages, so the better-sqlite3 addon installed above is kept as built.
RUN npm prune --omit=dev --no-audit --no-fund
# ── target: runtime ── app only, never touches the docs repo ────────────────
FROM node:24.22.1-bookworm-slim AS runtime
ENV NODE_ENV=production
WORKDIR /app
COPY package.json package-lock.json ./
COPY --from=build /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
COPY --from=build /app/skills ./skills
COPY --from=build /app/scripts ./scripts
# scripts/build-index.ts runs under tsx both here (baking the index) and at
# runtime (in-container refresh), and it imports ../src/lib/*. The server runs
# from dist/, but the build script needs the TypeScript sources it imports, or
# every index build fails with ERR_MODULE_NOT_FOUND.
COPY --from=build /app/src ./src
# git is required at RUNTIME for the in-container index refresh
# (INDEX_AUTO_REFRESH): the sparse, blobless clone fetches only the .mdx and
# .md blobs (~44MB) — a codeload tarball of the docs repo is ~390MB and cannot
# be filtered.
RUN apt-get update -qq \
&& apt-get install -y -qq --no-install-recommends git ca-certificates >/dev/null \
&& rm -rf /var/lib/apt/lists/*
# /app/data/generations is the index-refresh work dir (INDEX_WORK_DIR) and the
# usual mount point for its volume. It must exist AND be owned by `node` so the
# non-root user can write to it when nothing is mounted, or on a Docker named
# volume (which copies this ownership). Orchestrator volumes generally do NOT
# inherit it: on Fargate use an init-perms container, on Kubernetes fsGroup.
RUN mkdir -p /app/data/generations && chown -R node:node /app/data
EXPOSE 3000
USER node
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
CMD node -e "fetch('http://127.0.0.1:3000/health').then(r=>process.exit(r.ok?0:1))"
CMD ["node", "dist/server.js"]
# ── indexer ── builds the index; only runs for --target full ────────────────
FROM runtime AS indexer
USER root
ARG DOCS_REPO_URL=https://github.com/cometchat/docs.git
ARG DOCS_REF=main
# DOCS_COMMIT is the cache-buster AND the provenance label: pass the full docs
# sha. A new sha forces a fresh clone + index; an unchanged sha legitimately
# reuses the cached one. The guard below refuses the default rather than let a
# warm cache silently ship a stale index. (A moving ref such as "main" would
# pass the guard but is a constant cache key, so don't.)
ARG DOCS_COMMIT=unpinned
# Sparse, blobless clone of top-level files + *.mdx and *.md anywhere ('/*'
# keeps root files, '!/*/' drops directories, the two globs re-include the docs
# tree; .md because a page may import one as a snippet, see
# src/search/mdx-imports.ts), then check out exactly DOCS_COMMIT so the label
# on `full` matches what was indexed.
# tsx by path, not npx: if tsx were missing, npx in a non-TTY build installs
# whatever tsx the registry serves instead of failing (refresher.ts uses the
# same path).
RUN test "${DOCS_COMMIT}" != "unpinned" || { echo "ERROR: --build-arg DOCS_COMMIT=<sha> is required for --target full"; exit 1; } \
&& echo "baking index: ${DOCS_REPO_URL}@${DOCS_REF} (${DOCS_COMMIT})" \
&& git clone --depth 1 --filter=blob:none --sparse --branch "${DOCS_REF}" "${DOCS_REPO_URL}" /tmp/docs \
&& git -C /tmp/docs sparse-checkout set --no-cone '/*' '!/*/' '**/*.mdx' '**/*.md' \
&& git -C /tmp/docs fetch --depth 1 origin "${DOCS_COMMIT}" \
&& git -C /tmp/docs checkout "${DOCS_COMMIT}" \
&& DOCS_REPO=/tmp/docs INDEX_PATH=/app/data/index.sqlite node_modules/.bin/tsx scripts/build-index.ts \
&& rm -rf /tmp/docs
# ── target: full ── runtime + baked index ───────────────────────────────────
FROM runtime AS full
ARG DOCS_COMMIT=unpinned
LABEL com.cometchat.docs-commit="${DOCS_COMMIT}"
COPY --from=indexer --chown=node:node /app/data/index.sqlite /app/data/index.sqlite