mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-18 13:06:54 +00:00
43a3a5217e
The runtime stage installed python3 and build-essential for one reason: when better-sqlite3 12.x could not download its prebuilt binary, node-gyp built the addon against the headers of whatever Node the base image carried that day. Since Node 24.19 those headers give node::ObjectWrap a cleanup hook that the runtime cannot unregister, and the first garbage-collected statement takes the process down with "Assertion failed: (env) != nullptr". The published image was spared only because its prebuild download had succeeded (issue #2421). 13.x is built on N-API, so it never touches that path, and it ships the binaries for amd64 and arm64 inside the npm tarball. Nothing is compiled any more, and the toolchain leaves the Dockerfile. The install runs with --ignore-scripts: a lockfile-driven npm ci still synthesises `node-gyp rebuild` for the binding.gyp it finds and ignores the package's gypfile:false (npm/cli#9837), which without the toolchain would fail the build. Closes #2421
142 lines
7.8 KiB
Docker
142 lines
7.8 KiB
Docker
# ── Stage 0: gosu ────────────────────────────────────────────────────────────
|
|
# Rebuild gosu with a current Go toolchain so the runtime image ships no stale
|
|
# Go stdlib (Debian's apt gosu is built with an old Go that trips CVE scanners).
|
|
# The binary and its runtime behaviour are identical to the apt package.
|
|
FROM golang:1.25-alpine AS gosu-build
|
|
RUN CGO_ENABLED=0 GOBIN=/out go install github.com/tianon/gosu@latest
|
|
|
|
# ── Stage 1: shared ──────────────────────────────────────────────────────────
|
|
FROM node:24-alpine AS shared-builder
|
|
WORKDIR /app
|
|
COPY package.json package-lock.json ./
|
|
COPY shared/package.json ./shared/
|
|
RUN npm ci --workspace=shared
|
|
COPY shared/ ./shared/
|
|
RUN npm run build --workspace=shared
|
|
|
|
# ── Stage 2: client ──────────────────────────────────────────────────────────
|
|
FROM node:24-alpine AS client-builder
|
|
WORKDIR /app
|
|
COPY package.json package-lock.json ./
|
|
COPY shared/package.json ./shared/
|
|
COPY client/package.json ./client/
|
|
COPY client/scripts/patch-maplibre.mjs ./client/scripts/
|
|
RUN npm ci --workspace=client
|
|
COPY --from=shared-builder /app/shared/dist ./shared/dist
|
|
COPY client/ ./client/
|
|
RUN npm run build --workspace=client
|
|
|
|
# ── Stage 3: server ──────────────────────────────────────────────────────────
|
|
# --ignore-scripts: nothing here needs a postinstall, tsc only reads types. The one
|
|
# native module, better-sqlite3, ships its binaries inside the npm tarball since 13.x.
|
|
FROM node:24-alpine AS server-builder
|
|
WORKDIR /app
|
|
COPY package.json package-lock.json ./
|
|
COPY shared/package.json ./shared/
|
|
COPY server/package.json ./server/
|
|
RUN npm ci --workspace=server --ignore-scripts
|
|
COPY --from=shared-builder /app/shared/dist ./shared/dist
|
|
COPY server/ ./server/
|
|
RUN npm run build --workspace=server
|
|
|
|
# ── Stage 4: production runtime ──────────────────────────────────────────────
|
|
FROM node:24-trixie-slim
|
|
WORKDIR /app
|
|
|
|
# Workspace manifests only — source never enters this stage.
|
|
COPY package.json package-lock.json ./
|
|
COPY shared/package.json ./shared/
|
|
COPY server/package.json ./server/
|
|
|
|
# The trailing chown runs in this layer on purpose: it covers the manifests and
|
|
# the freshly installed node_modules while they are already part of this layer's
|
|
# changeset, so it costs nothing. Everything copied after this point carries
|
|
# --chown=node:node for the same reason — a recursive chown in a later layer
|
|
# would copy up every inode it touches and duplicate the whole tree in the image.
|
|
#
|
|
# No compiler in this stage. better-sqlite3 13.x carries prebuilt binaries for
|
|
# amd64 and arm64 inside its tarball, so there is nothing left to build: the old
|
|
# python3 + build-essential fallback compiled it against whatever Node headers
|
|
# the base image had that day, and 24.19+ headers give an addon that aborts with
|
|
# "Assertion failed: (env) != nullptr" the first time a statement is collected
|
|
# (#2421). --ignore-scripts because a lockfile-driven npm ci still synthesises
|
|
# `node-gyp rebuild` for the binding.gyp it finds, blind to the package's
|
|
# gypfile:false (npm/cli#9837). The only other install scripts among the runtime
|
|
# deps are esbuild's binary check, redundant next to its optional platform
|
|
# package, and a telemetry ping.
|
|
RUN apt-get update && \
|
|
apt-get install -y --no-install-recommends tzdata dumb-init wget ca-certificates \
|
|
libkitinerary-bin libsqlite3-0 && \
|
|
npm ci --workspace=server --omit=dev --ignore-scripts && \
|
|
ln -sf "$(find /usr/lib -name kitinerary-extractor -type f | head -1)" /usr/local/bin/kitinerary-extractor; \
|
|
rm -rf /var/lib/apt/lists/* /usr/local/lib/node_modules/npm /usr/local/bin/npm /usr/local/bin/npx && \
|
|
chown -R node:node /app
|
|
|
|
# gosu rebuilt with a current Go toolchain (stage 0) — the entrypoint script uses
|
|
# it to drop to node.
|
|
COPY --from=gosu-build /out/gosu /usr/local/bin/gosu
|
|
|
|
ENV XDG_CACHE_HOME=/tmp/kf6-cache
|
|
# Prevent Qt from probing for a display in headless containers.
|
|
ENV QT_QPA_PLATFORM=offscreen
|
|
# Fixed path for both amd64 (static binary) and arm64 (symlink to apt binary).
|
|
# Override with KITINERARY_EXTRACTOR_PATH if you install it elsewhere.
|
|
ENV KITINERARY_EXTRACTOR_PATH=/usr/local/bin/kitinerary-extractor
|
|
|
|
COPY --chown=node:node --from=server-builder /app/server/dist ./server/dist
|
|
# Runtime data assets read from server/assets at runtime: airports.json (flight
|
|
# transport search) and atlas/*.geojson.gz (Atlas country/region map). The build
|
|
# only emits dist, so these must be copied explicitly or the features silently
|
|
# degrade to empty in the image.
|
|
COPY --chown=node:node --from=server-builder /app/server/assets ./server/assets
|
|
# The in-app help pages (/help) read this straight from disk at runtime, so the
|
|
# docs always match the version running. Without it, wikiService falls back to
|
|
# fetching the GitHub wiki, which tracks main and needs network access.
|
|
COPY --chown=node:node wiki ./wiki
|
|
# tsconfig-paths/register reads this at runtime to resolve MCP SDK paths.
|
|
COPY --chown=node:node server/tsconfig.json ./server/
|
|
# Encryption-key rotation is run on demand via tsx (a prod dep) straight from the
|
|
# raw .ts source — it never enters dist, so it must be copied in explicitly or
|
|
# `node --import tsx scripts/migrate-encryption.ts` fails with module-not-found.
|
|
COPY --chown=node:node server/scripts/migrate-encryption.ts ./server/scripts/migrate-encryption.ts
|
|
# Admin recovery script (node server/reset-admin.js) for locked-out installs.
|
|
COPY --chown=node:node server/reset-admin.js ./server/reset-admin.js
|
|
COPY --chown=node:node --from=shared-builder /app/shared/dist ./shared/dist
|
|
COPY --chown=node:node --from=client-builder /app/client/dist ./server/public
|
|
COPY --chown=node:node --from=client-builder /app/client/public/fonts ./server/public/fonts
|
|
|
|
# journey/ and places/ must be listed here and in server/src/index.ts (#1762) —
|
|
# a dir created lazily on first upload needs write permission on the uploads
|
|
# mount point itself, which fails with EACCES when the bind-mounted host dir
|
|
# isn't writable by node. Only paths this layer creates are chowned; anything
|
|
# already in the image arrived node-owned via --chown above.
|
|
RUN mkdir -p /app/data/logs /app/uploads/files /app/uploads/covers /app/uploads/avatars \
|
|
/app/uploads/photos /app/uploads/journey /app/uploads/places && \
|
|
ln -s /app/uploads /app/server/uploads && \
|
|
ln -s /app/data /app/server/data && \
|
|
chown -R node:node /app/data /app/uploads && \
|
|
chown -h node:node /app/server/uploads /app/server/data
|
|
|
|
ENV NODE_ENV=production
|
|
ENV NODE_USE_ENV_PROXY=1
|
|
ENV PORT=3000
|
|
ARG APP_VERSION=dev
|
|
ENV APP_VERSION=${APP_VERSION}
|
|
|
|
EXPOSE 3000
|
|
|
|
HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \
|
|
CMD wget -qO- http://localhost:3000/api/health || exit 1
|
|
|
|
# Start-up lives in a script, not an inline `sh -c` string: container management
|
|
# UIs re-tokenise Config.Cmd when you edit a container in place, and a command
|
|
# carrying quotes does not survive that round-trip (the container then dies with
|
|
# a shell syntax error before Node starts). A CMD of one bare token does.
|
|
COPY server/scripts/entrypoint.sh /usr/local/bin/trek-entrypoint
|
|
# Root-owned on purpose — this is what calls gosu, so it runs before the drop to
|
|
# node. chmod is explicit because a checkout on Windows carries no exec bit.
|
|
RUN chmod 0755 /usr/local/bin/trek-entrypoint
|
|
|
|
ENTRYPOINT ["dumb-init", "--"]
|
|
CMD ["/usr/local/bin/trek-entrypoint"]
|