Files

58 lines
3.0 KiB
Docker

# 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