ARG POSTHOG_HOST=https://app.posthog.com ARG POSTHOG_API_KEY=posthog-api-key ARG INTERCOM_ID=intercom-id ARG INFISICAL_CLI_VERSION=0.45.4 FROM node:26.12.1-trixie-slim AS base # Node 26.12.1 bundles npm 11.20.0, whose own dependencies are outdated. 11.21.1 updates them # and stays on the npm major that Node ships. Its bundled ip-address and undici are swapped for # in-range patch releases. The swap fails if npm bundles other versions; drop it once an npm # release bundles the patched ones. RUN npm install -g npm@11.21.1 \ && cd /tmp \ && NPM_DEPS=/usr/local/lib/node_modules/npm/node_modules \ && for dep in ip-address@10.7.2 undici@6.29.0; do \ bundled="$(node -p "require('$NPM_DEPS/${dep%@*}/package.json').version")"; \ [ "$bundled" = "${dep##*@}" ] || { echo "npm bundles ${dep%@*} $bundled, expected ${dep##*@}" >&2; exit 1; }; \ done \ && npm pack ip-address@10.7.4 undici@6.29.1 --silent \ && rm -rf "$NPM_DEPS/ip-address" "$NPM_DEPS/undici" \ && mkdir "$NPM_DEPS/ip-address" "$NPM_DEPS/undici" \ && tar -xzf ip-address-10.7.4.tgz -C "$NPM_DEPS/ip-address" --strip-components=1 --no-same-owner \ && tar -xzf undici-6.29.1.tgz -C "$NPM_DEPS/undici" --strip-components=1 --no-same-owner \ && rm ip-address-10.7.4.tgz undici-6.29.1.tgz \ && npm cache clean --force FROM base AS frontend-dependencies WORKDIR /app COPY frontend/package.json frontend/package-lock.json ./ # Install dependencies RUN npm ci --ignore-scripts # Rebuild the source code only when needed FROM base AS frontend-builder WORKDIR /app # Copy dependencies COPY --from=frontend-dependencies /app/node_modules ./node_modules # Copy all files COPY /frontend . ENV NODE_ENV production ARG POSTHOG_HOST ENV VITE_POSTHOG_HOST $POSTHOG_HOST ARG POSTHOG_API_KEY ENV VITE_POSTHOG_API_KEY $POSTHOG_API_KEY ARG INTERCOM_ID ENV VITE_INTERCOM_ID $INTERCOM_ID ARG INFISICAL_PLATFORM_VERSION ENV VITE_INFISICAL_PLATFORM_VERSION $INFISICAL_PLATFORM_VERSION ENV NODE_OPTIONS="--max-old-space-size=8192" # Build RUN npm run build # Production image FROM base AS frontend-runner WORKDIR /app RUN groupadd -r -g 1001 nodejs && useradd -r -u 1001 -g nodejs non-root-user COPY --from=frontend-builder --chown=non-root-user:nodejs /app/dist ./ USER 1001 ## ## BACKEND ## # Oracle Instant Client (parallel stage — downloads while other stages build) FROM debian:trixie-slim AS oracle RUN apt-get update && apt-get install -y unzip wget ca-certificates \ && rm -rf /var/lib/apt/lists/* RUN ARCH=$(dpkg --print-architecture) && \ if [ "$ARCH" = "amd64" ]; then \ ORACLE_ZIP="instantclient-basic-linux.x64-23.26.0.0.0.zip" && \ EXPECTED_SHA="d6c79cbcf0ff209363e779855c690d4fc730aed847e9198a2c439bcf34760af5"; \ elif [ "$ARCH" = "arm64" ]; then \ ORACLE_ZIP="instantclient-basic-linux.arm64-23.26.0.0.0.zip" && \ EXPECTED_SHA="9c9a32051e97f087016fb334b7ad5c0aea8511ca8363afd8e0dc6ec4fc515c32"; \ fi && \ ORACLE_URL="https://download.oracle.com/otn_software/linux/instantclient/2326000/${ORACLE_ZIP}" && \ wget -q "$ORACLE_URL" && \ echo "$EXPECTED_SHA $ORACLE_ZIP" | sha256sum -c - && \ mkdir -p /opt/oracle && \ unzip "$ORACLE_ZIP" -d /opt/oracle && \ rm "$ORACLE_ZIP" FROM base AS backend-build ENV ChrystokiConfigurationPath=/usr/safenet/lunaclient/ RUN groupadd -r -g 1001 nodejs && useradd -r -u 1001 -g nodejs non-root-user WORKDIR /app # Required for pkcs11js and ODBC RUN apt-get update && apt-get install -y \ python3 \ make \ g++ \ unixodbc \ unixodbc-dev \ freetds-dev \ freetds-bin \ tdsodbc \ && rm -rf /var/lib/apt/lists/* # Configure ODBC RUN printf "[FreeTDS]\nDescription = FreeTDS Driver\nDriver = /usr/lib/x86_64-linux-gnu/odbc/libtdsodbc.so\nSetup = /usr/lib/x86_64-linux-gnu/odbc/libtdsS.so\nFileUsage = 1\n" > /etc/odbcinst.ini COPY backend/package*.json ./ RUN npm ci COPY /backend . COPY --chown=non-root-user:nodejs standalone-entrypoint.sh standalone-entrypoint.sh RUN npm run build # Bake in-app announcements + their images at build time so self-hosted / air-gapped # instances don't make any outbound calls to Contentful at runtime. No-ops silently # if Contentful secrets are not supplied (cloud / dev builds). Secrets are passed # via BuildKit --mount=type=secret so they never enter any image layer or SLSA # provenance attestation. RUN --mount=type=secret,id=contentful_space_id,required=false \ --mount=type=secret,id=contentful_delivery_token,required=false \ --mount=type=secret,id=contentful_environment,required=false \ CONTENTFUL_SPACE_ID="$(cat /run/secrets/contentful_space_id 2>/dev/null || true)" \ CONTENTFUL_DELIVERY_TOKEN="$(cat /run/secrets/contentful_delivery_token 2>/dev/null || true)" \ CONTENTFUL_ENVIRONMENT="$(cat /run/secrets/contentful_environment 2>/dev/null || true)" \ npm run bake:announcements # Production stage FROM base AS backend-runner ENV ChrystokiConfigurationPath=/usr/safenet/lunaclient/ WORKDIR /app # Required for pkcs11js and ODBC RUN apt-get update && apt-get install -y \ python3 \ make \ g++ \ unixodbc \ unixodbc-dev \ freetds-dev \ freetds-bin \ tdsodbc \ && rm -rf /var/lib/apt/lists/* # Configure ODBC RUN printf "[FreeTDS]\nDescription = FreeTDS Driver\nDriver = /usr/lib/x86_64-linux-gnu/odbc/libtdsodbc.so\nSetup = /usr/lib/x86_64-linux-gnu/odbc/libtdsS.so\nFileUsage = 1\n" > /etc/odbcinst.ini COPY --from=backend-build /app . RUN rm -rf ./node_modules ./bdd ./e2e-test ./src RUN npm ci --omit=dev # Test folders these packages publish, with sample private keys that image scanners flag RUN rm -rf node_modules/@fastify/reply-from/test node_modules/ssh2/test node_modules/universal-github-app-jwt/test RUN mkdir frontend-build # Production stage FROM base AS production # Install all required runtime packages ARG INFISICAL_CLI_VERSION RUN apt-get update && apt-get install -y --no-install-recommends \ build-essential \ autoconf \ automake \ libtool \ wget \ perl \ libssl-dev \ ca-certificates \ curl \ git \ python3 \ make \ g++ \ unixodbc \ unixodbc-dev \ freetds-dev \ freetds-bin \ tdsodbc \ libaio1t64 \ smbclient \ openssh-client \ && curl -1sLf 'https://artifacts-cli.infisical.com/setup.deb.sh' | bash \ && test -n "$INFISICAL_CLI_VERSION" \ && apt-get update && apt-get install -y infisical="$INFISICAL_CLI_VERSION" \ # Upgrade base-image packages that apt will not move on its own. Only packages # preinstalled in the base belong here; anything pulled in as a dependency # already installs at the candidate version. Re-check each release; a stale # pin holds the vulnerable version, and a withdrawn one fails the build. && apt-get install -y --only-upgrade \ libgnutls30t64=3.8.9-3+deb13u4 \ libc6=2.41-12+deb13u4 \ libc-bin=2.41-12+deb13u4 \ libssl3t64=3.5.7-1~deb13u3 \ dpkg=1.22.22 \ apt=3.0.4 \ libapt-pkg7.0=3.0.4 \ libcap2 \ libnghttp2-14=1.64.0-1.1+deb13u1 \ libngtcp2-16=1.11.0-1+deb13u1 \ libngtcp2-crypto-gnutls8=1.11.0-1+deb13u1 \ libsqlite3-0=3.46.1-7+deb13u2 \ util-linux=2.41.5-0+deb13u1 \ mount=2.41.5-0+deb13u1 \ libblkid1=2.41.5-0+deb13u1 \ libuuid1=2.41.5-0+deb13u1 \ libmount1=2.41.5-0+deb13u1 \ libsmartcols1=2.41.5-0+deb13u1 \ liblastlog2-2=2.41.5-0+deb13u1 \ bsdutils=1:2.41.5-0+deb13u1 \ login=1:4.16.0-2+really2.41.5-0+deb13u1 \ libsystemd0=257.13-1~deb13u1 \ libudev1=257.13-1~deb13u1 \ sed=4.9-2+deb13u1 \ && rm -rf /var/lib/apt/lists/* # Copy Oracle Instant Client from parallel stage COPY --from=oracle /opt/oracle /opt/oracle RUN ARCH=$(dpkg --print-architecture) && \ if [ "$ARCH" = "amd64" ]; then \ ln -sf /lib/x86_64-linux-gnu/libaio.so.1t64 /lib/x86_64-linux-gnu/libaio.so.1; \ elif [ "$ARCH" = "arm64" ]; then \ ln -sf /lib/aarch64-linux-gnu/libaio.so.1t64 /lib/aarch64-linux-gnu/libaio.so.1; \ fi && \ echo /opt/oracle/instantclient_23_26 > /etc/ld.so.conf.d/oracle-instantclient.conf && \ ldconfig # Configure ODBC in production RUN printf "[FreeTDS]\nDescription = FreeTDS Driver\nDriver = /usr/lib/x86_64-linux-gnu/odbc/libtdsodbc.so\nSetup = /usr/lib/x86_64-linux-gnu/odbc/libtdsS.so\nFileUsage = 1\n" > /etc/odbcinst.ini WORKDIR /openssl-build RUN wget https://www.openssl.org/source/openssl-3.1.2.tar.gz \ && tar -xf openssl-3.1.2.tar.gz \ && cd openssl-3.1.2 \ && ./Configure enable-fips \ && make \ && make install_fips \ && cd / \ && rm -rf /openssl-build \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* # Build OpenSSL 3.5.9 for PQC (ML-DSA / SLH-DSA) certificate support. # Installed to /opt/openssl-pqc so it does not conflict with the FIPS OpenSSL above. WORKDIR /tmp/openssl-pqc-build RUN wget -q https://github.com/openssl/openssl/releases/download/openssl-3.5.9/openssl-3.5.9.tar.gz \ && echo "603f5602e2eef00d77fbd429d34dcd5822bb301757a1bc9cdb24c670f1eb859a openssl-3.5.9.tar.gz" | sha256sum -c - \ && tar -xf openssl-3.5.9.tar.gz \ && cd openssl-3.5.9 \ && ./Configure --prefix=/opt/openssl-pqc --openssldir=/opt/openssl-pqc/ssl no-docs \ && make -j"$(nproc)" \ && make install_sw \ && cd / \ && rm -rf /tmp/openssl-pqc-build RUN groupadd -r -g 1001 nodejs && useradd -r -u 1001 -g nodejs non-root-user # Give non-root-user permission to update SSL certs RUN chown -R non-root-user /etc/ssl/certs RUN chown non-root-user /etc/ssl/certs/ca-certificates.crt RUN chmod -R u+rwx /etc/ssl/certs RUN chmod u+rw /etc/ssl/certs/ca-certificates.crt RUN chown non-root-user /usr/sbin/update-ca-certificates RUN chmod u+rx /usr/sbin/update-ca-certificates ## set pre baked keys ARG POSTHOG_API_KEY ENV POSTHOG_API_KEY=$POSTHOG_API_KEY ARG INTERCOM_ID=intercom-id ENV INTERCOM_ID=$INTERCOM_ID WORKDIR / COPY --from=backend-runner /app /backend COPY --from=frontend-runner /app ./backend/frontend-build # Make export-assets script executable for CDN asset extraction RUN chmod +x /backend/scripts/export-assets.sh ARG INFISICAL_PLATFORM_VERSION ENV INFISICAL_PLATFORM_VERSION $INFISICAL_PLATFORM_VERSION ENV PORT 8080 ENV HOST=0.0.0.0 ENV HTTPS_ENABLED false ENV NODE_ENV production ENV STANDALONE_BUILD true ENV STANDALONE_MODE true ENV ChrystokiConfigurationPath=/usr/safenet/lunaclient/ ENV NODE_OPTIONS="--max-old-space-size=8192 --force-fips" # FIPS mode of operation: ENV OPENSSL_CONF=/backend/nodejs.fips.cnf ENV OPENSSL_MODULES=/usr/local/lib/ossl-modules ENV FIPS_ENABLED=true WORKDIR /backend ENV TELEMETRY_ENABLED true EXPOSE 8080 EXPOSE 443 # Remove telemetry. dd-trace uses BullMQ with MD5 hashing, which breaks when FIPS mode is enabled. RUN grep -v 'import "./lib/telemetry/instrumentation.mjs";' dist/main.mjs > dist/main.mjs.tmp && \ mv dist/main.mjs.tmp dist/main.mjs # The OpenSSL library is installed in different locations in different architectures (x86_64 and arm64). # This is a workaround to avoid errors when the library is not found. RUN ln -sf /usr/local/lib64/ossl-modules /usr/local/lib/ossl-modules || \ ln -sf /usr/local/lib/ossl-modules /usr/local/lib64/ossl-modules # Use numeric UID so Kubernetes can verify the image runs as non-root. USER 1001 CMD ["./standalone-entrypoint.sh"]