# Build the operator console, then serve the built assets. The previous image ran `vite dev` as # root with the whole source tree inside it, which is a development server rather than a release # artefact: it rebuilds on request, exposes the module graph and needs write access to its own # source. M16 replaces it with a static build served by an unprivileged nginx. FROM node:22-alpine@sha256:c610fcdfb1d5b4740dd70c284ed3cb16bb857e0f7166196e36a5501df7a3aa32 AS build WORKDIR /app RUN apk upgrade --no-cache COPY package.json package-lock.json ./ RUN npm ci COPY . . # Vite inlines its VITE_* variables at build time, so the API base URL is a build argument rather # than a runtime one. The default matches the standard local deployment. ARG VITE_API_BASE_URL=http://localhost:8000 ENV VITE_API_BASE_URL=${VITE_API_BASE_URL} RUN npm run build FROM nginxinc/nginx-unprivileged:1.29-alpine@sha256:0c79d56aee561a1d81c63f00eee5fb5fe29279560cdc55e91425133104c7fbe6 USER root RUN apk upgrade --no-cache ARG VITE_API_BASE_URL=http://localhost:8000 ARG MODELFORGE_VERSION=0.0.0 ARG MODELFORGE_COMMIT="" ARG MODELFORGE_BUILT_AT="" ENV MODELFORGE_BUILD_COMMIT=${MODELFORGE_COMMIT} ENV MODELFORGE_BUILD_TIMESTAMP=${MODELFORGE_BUILT_AT} LABEL org.opencontainers.image.title="ITWorx ModelForge operator console" LABEL org.opencontainers.image.description="Operator console for the ITWorx ModelForge control plane" LABEL org.opencontainers.image.version="${MODELFORGE_VERSION}" LABEL org.opencontainers.image.revision="${MODELFORGE_COMMIT}" LABEL org.opencontainers.image.created="${MODELFORGE_BUILT_AT}" LABEL org.opencontainers.image.source="https://git.example.com/example/modelforge.git" LABEL org.opencontainers.image.vendor="ITWorx" LABEL org.opencontainers.image.licenses="AGPL-3.0-or-later" # The runtime listens above 1024 and returns to the image's unprivileged nginx user after the # signed package upgrade and immutable file assembly. It works with a read-only root filesystem # plus a tmpfs for its caches. COPY --from=build /app/dist /usr/share/nginx/html COPY nginx.conf /etc/nginx/conf.d/default.conf COPY security-headers.inc.template /etc/nginx/security-headers.inc.template # The bundle's API origin is compiled in at build time, so the connect-src that protects it is # derived from the same argument rather than maintained separately and allowed to drift. Only the # origin is used; a path in connect-src is ignored by the browser anyway. The final grep makes a # failed substitution break the build instead of shipping a policy with a placeholder in it. RUN set -eu; \ API_ORIGIN=$(printf '%s' "${VITE_API_BASE_URL}" | cut -d/ -f1-3); \ sed "s|__API_ORIGIN__|${API_ORIGIN}|" /etc/nginx/security-headers.inc.template \ > /etc/nginx/conf.d/security-headers.inc; \ rm /etc/nginx/security-headers.inc.template; \ grep -q "connect-src 'self' ${API_ORIGIN};" /etc/nginx/conf.d/security-headers.inc USER nginx EXPOSE 3000 HEALTHCHECK --interval=15s --timeout=3s --retries=5 \ CMD wget -q -O /dev/null http://127.0.0.1:3000/ || exit 1