mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-18 21:16:47 +00:00
c6b6b83d38
Portainer's recreate and a copied run config carry the previous image's APP_VERSION into a container built from the new one, so the server kept announcing the old release and clients never handed over to the new bundle. The Dockerfile now writes the version into server/VERSION, which the server reads before APP_VERSION, and prerelease bundles take their version from the same build argument.
162 lines
9.0 KiB
Docker
162 lines
9.0 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/
|
|
# A prerelease build does not bump client/package.json, so the bundle takes its
|
|
# version from the same build argument the server's VERSION file comes from.
|
|
ARG APP_VERSION=dev
|
|
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
|
|
# The version goes into a file, not into ENV. A container keeps its environment
|
|
# when it is recreated on a newer image (Portainer's recreate, a copied run
|
|
# config), so an APP_VERSION env var would keep announcing the old release and
|
|
# the clients would never pick up the new bundle. The server reads this file
|
|
# first (server/src/app-config/image-version.ts).
|
|
ARG APP_VERSION=dev
|
|
RUN if [ "$APP_VERSION" != "dev" ]; then printf '%s
|
|
' "$APP_VERSION" > /app/server/VERSION; fi
|
|
|
|
# OCI metadata: Renovate, Watchtower and the registries read the source label to
|
|
# link an image update to its release notes (#1498). The release workflows add
|
|
# the commit and the build time; a local build carries everything else.
|
|
LABEL org.opencontainers.image.title="TREK" \
|
|
org.opencontainers.image.description="Self-hosted collaborative travel planner" \
|
|
org.opencontainers.image.url="https://github.com/liketrek/TREK" \
|
|
org.opencontainers.image.source="https://github.com/liketrek/TREK" \
|
|
org.opencontainers.image.documentation="https://github.com/liketrek/TREK/wiki" \
|
|
org.opencontainers.image.licenses="AGPL-3.0-only" \
|
|
org.opencontainers.image.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"]
|