Repository navigation
Expand file tree
/
Copy pathDockerfile
More file actions
130 lines (104 loc) · 6.28 KB
/
Copy pathDockerfile
File metadata and controls
130 lines (104 loc) · 6.28 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
# =============================================================================
# World Monitor — Docker Image
# =============================================================================
# Multi-stage build:
# builder — installs deps, compiles TS handlers, builds Vite frontend
# runtime-deps — installs only packages needed by unbundled raw JS handlers
# final — nginx (static) + node (API) under supervisord
# =============================================================================
# ── Stage 1: Builder ─────────────────────────────────────────────────────────
FROM node:24-alpine@sha256:feb76650b7195319268c59cb2d419c732d6c499b13bf5296f16ab59eb1f0a4f3 AS builder
WORKDIR /app
ENV npm_config_install_links=true
# Install root dependencies (layer-cached until package.json changes)
COPY package.json package-lock.json ./
COPY vendor/braces/ ./vendor/braces/
COPY vendor/stream-json/ ./vendor/stream-json/
RUN npm ci --ignore-scripts
# Copy full source
COPY . .
# The crawlable-corpus step runs the source-attribution drift gate against
# scripts/, server/, api/, and src/. generate-inventory-facts and
# build-handlers write untracked .js into those same roots, so the gate must
# run on the pristine checkout first (#7435). tsc + vite stay later: they
# need the generated inventory assets and compiled handlers.
RUN npm run build:crawlable-corpus && npm run build:sitemap
# Generated inventory modules are intentionally untracked. Recreate them in
# the clean image context before handlers import or bundle them.
RUN node scripts/generate-inventory-facts.mjs
# Compile TypeScript API handlers → self-contained ESM bundles
# Output is api/**/*.js alongside the source .ts files
RUN node docker/build-handlers.mjs
# public/pro/ is a build product, not committed bytes (#6898), so this image has
# to build it. Skipping it does NOT 404: this image installs docker/nginx.conf,
# whose `location /` ends in `try_files $uri $uri/ /dashboard.html`,
# so /pro would quietly serve the dashboard SPA shell with a 200 — wrong content
# under a real URL, which is worse than a missing page. (docker/Dockerfile is the
# one with an explicit `location ^~ /pro` block, in nginx.conf.template.)
# build:pro installs pro-test's own lockfile.
RUN npm run build:pro
# Build the Vite frontend (outputs to dist/)
# Skip blog build — blog-site has its own deps not installed here
RUN npx tsc && npx vite build
# Assert the /pro pages survived the public/ -> dist/ copy (#6898). build:pro
# succeeding proves public/pro/ exists; it does NOT prove Vite copied it, and
# docker/nginx.conf's SPA fallback would serve the dashboard shell at 200 for a
# missing /pro rather than failing visibly.
RUN test -s dist/pro/index.html && test -s dist/pro/welcome.html
# ── Stage 2: Runtime dependencies ───────────────────────────────────────────
FROM node:24-alpine@sha256:feb76650b7195319268c59cb2d419c732d6c499b13bf5296f16ab59eb1f0a4f3 AS runtime-deps
WORKDIR /app
# Keep the runtime dependency set deliberately smaller than the app's full
# production graph. The raw api/*.js handlers are not bundled by
# docker/build-handlers.mjs, so they still need these package imports at
# runtime, but the frontend/server-only production deps do not belong in the
# final image.
COPY docker/runtime-package.json ./package.json
COPY docker/runtime-package-lock.json ./package-lock.json
RUN npm ci --omit=dev --omit=optional --ignore-scripts
# ── Stage 3: Runtime ─────────────────────────────────────────────────────────
FROM node:24-alpine@sha256:feb76650b7195319268c59cb2d419c732d6c499b13bf5296f16ab59eb1f0a4f3 AS final
# nginx + supervisord
RUN apk add --no-cache nginx supervisor gettext && \
mkdir -p /tmp/nginx-client-body /tmp/nginx-proxy /tmp/nginx-fastcgi \
/tmp/nginx-uwsgi /tmp/nginx-scgi /var/log/supervisor && \
addgroup -S appgroup && adduser -S appuser -G appgroup
WORKDIR /app
# API server
COPY --from=builder /app/src-tauri/sidecar/local-api-server.mjs ./local-api-server.mjs
COPY --from=builder /app/src-tauri/sidecar/package.json ./package.json
COPY --from=builder /app/shared/llm-health-providers.js ./shared/llm-health-providers.js
ENV LOCAL_API_RESOURCE_DIR=/app
# Minimal runtime node_modules — required by raw .js handlers that aren't
# bundled by build-handlers.mjs. Without this the Node sidecar dispatches
# those routes, fails to resolve package imports like @upstash/ratelimit,
# and returns 502 "missing dependency".
COPY --from=runtime-deps /app/node_modules ./node_modules
# API handler modules (JS originals + compiled TS bundles)
COPY --from=builder /app/api ./api
# Static data files used by handlers at runtime
COPY --from=builder /app/data ./data
# Built frontend static files
COPY --from=builder /app/dist /usr/share/nginx/html
# Nginx + supervisord configs
COPY docker/nginx.conf /etc/nginx/nginx.conf.template
COPY docker/supervisord.conf /etc/supervisor/conf.d/worldmonitor.conf
COPY docker/entrypoint.sh /app/entrypoint.sh
COPY docker/render-nginx-realip.mjs /app/render-nginx-realip.mjs
COPY docker/validate-session-secret.mjs /app/validate-session-secret.mjs
RUN chmod +x /app/entrypoint.sh
# Ensure writable dirs for non-root
RUN chown -R appuser:appgroup /app /tmp/nginx-client-body /tmp/nginx-proxy \
/tmp/nginx-fastcgi /tmp/nginx-uwsgi /tmp/nginx-scgi /var/log/supervisor \
/var/lib/nginx /var/log/nginx
USER appuser
EXPOSE 8080
# Healthcheck via nginx. Use 127.0.0.1 (not localhost - that resolves to ::1
# first, where nginx does not listen). Probe /api/sidecar-health, a dedicated
# auth-exempt liveness route in the sidecar (local-api-server.mjs): reaching it
# through nginx's /api/ proxy verifies BOTH nginx and the node sidecar are up,
# unlike a static "/" probe which only proves nginx is serving. Keep this off
# /api/health so the public compact data-health contract still reaches api/health.js.
HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \
CMD wget -qO- http://127.0.0.1:8080/api/sidecar-health >/dev/null 2>&1 || exit 1
CMD ["/app/entrypoint.sh"]