# syntax=docker/dockerfile:1.19 # base node image FROM --platform=linux/amd64 node:24.18.0-bookworm-slim@sha256:0778d035a13f3f3833b7f2cb750e0df6cbce45583e84fd822f499f0c902a6c74 AS base # set for base and all layer that inherit from it ENV NODE_ENV="production" ENV CI="true" ENV PNPM_HOME="/pnpm" ENV PATH="$PNPM_HOME:$PATH" # Install OpenSSL for Prisma. Health checks use Node's built-in fetch so the # runtime image does not need curl and its otherwise unused dependency tree. # Build stages use pnpm exclusively; removing npm/npx drops an otherwise # unreachable vulnerable dependency tree from every final image. RUN apt-get update \ && apt-get install -y --no-install-recommends openssl \ && rm -rf /var/lib/apt/lists/* \ && corepack enable \ && corepack prepare pnpm@11.12.0 --activate \ && rm -rf /usr/local/lib/node_modules/npm /usr/local/bin/npm /usr/local/bin/npx # Install all node_modules, including dev dependencies FROM base AS deps WORKDIR /myapp ADD package.json pnpm-lock.yaml pnpm-workspace.yaml ./ ADD prisma prisma COPY apps/api/package.json apps/api/package.json COPY apps/hocuspocus/package.json apps/hocuspocus/package.json COPY apps/mcp/package.json apps/mcp/package.json COPY apps/portal-domain-controller/package.json apps/portal-domain-controller/package.json COPY apps/web/package.json apps/web/package.json COPY apps/worker/package.json apps/worker/package.json COPY packages/activity/package.json packages/activity/package.json COPY packages/api-client-ts/package.json packages/api-client-ts/package.json COPY packages/api-types/package.json packages/api-types/package.json COPY packages/brand/package.json packages/brand/package.json COPY packages/billing/package.json packages/billing/package.json COPY packages/captcha/package.json packages/captcha/package.json COPY packages/chat/package.json packages/chat/package.json COPY packages/cli/package.json packages/cli/package.json COPY packages/config/package.json packages/config/package.json COPY packages/db/package.json packages/db/package.json COPY packages/email-sequences/package.json packages/email-sequences/package.json COPY packages/email-templates/package.json packages/email-templates/package.json COPY packages/entitlements/package.json packages/entitlements/package.json COPY packages/error-reporter/package.json packages/error-reporter/package.json COPY packages/exports/package.json packages/exports/package.json COPY packages/feature-flags/package.json packages/feature-flags/package.json COPY packages/form-widget/package.json packages/form-widget/package.json COPY packages/foundation/package.json packages/foundation/package.json COPY packages/i18n/package.json packages/i18n/package.json COPY packages/imports/package.json packages/imports/package.json COPY packages/integrations/package.json packages/integrations/package.json COPY packages/logger/package.json packages/logger/package.json COPY packages/mail/package.json packages/mail/package.json COPY packages/marketing-outbound/package.json packages/marketing-outbound/package.json COPY packages/metrics/package.json packages/metrics/package.json COPY packages/observability/package.json packages/observability/package.json COPY packages/plugin-runtime/package.json packages/plugin-runtime/package.json COPY packages/plugin-sdk/package.json packages/plugin-sdk/package.json COPY packages/queue/package.json packages/queue/package.json COPY packages/realtime/package.json packages/realtime/package.json COPY packages/search/package.json packages/search/package.json COPY packages/shared-store/package.json packages/shared-store/package.json COPY packages/shared/package.json packages/shared/package.json COPY packages/storage/package.json packages/storage/package.json COPY packages/ui-app/package.json packages/ui-app/package.json COPY packages/ui-mock/package.json packages/ui-mock/package.json COPY packages/ui-subframe/package.json packages/ui-subframe/package.json COPY packages/ui/package.json packages/ui/package.json COPY packages/url/package.json packages/url/package.json COPY packages/view-models/package.json packages/view-models/package.json COPY runtime/api/package.json runtime/api/package.json COPY runtime/hocuspocus/package.json runtime/hocuspocus/package.json COPY runtime/mcp/package.json runtime/mcp/package.json COPY runtime/portal-domain-controller/package.json runtime/portal-domain-controller/package.json COPY runtime/web/package.json runtime/web/package.json COPY runtime/worker/package.json runtime/worker/package.json # patchedDependencies (package.json) referenzieren Dateien in patches/ — diese muessen vor # `pnpm fetch`/`pnpm install` im Build-Context liegen, sonst ERR_PNPM_PATCH_NOT_FOUND. COPY patches ./patches # Dependency installation and runtime-closure deployment reuse the same locked # store. The closure stage below takes that lock once, runs its five offline # deploys concurrently and refreshes only after a failed offline wave. RUN --mount=type=cache,id=hivegrid-pnpm-store-v2,target=/pnpm/store,sharing=locked \ pnpm fetch --frozen-lockfile RUN --mount=type=cache,id=hivegrid-pnpm-store-v2,target=/pnpm/store,sharing=locked \ pnpm install --frozen-lockfile --prod=false --offline # Prepare shared build inputs. Runtime-specific stages below own their # target builds so web build args do not bust API/worker cache paths. FROM base AS build ENV DATABASE_URL="postgresql://hivegrid:hivegrid@localhost:5432/hivegrid?schema=public" ENV NODE_OPTIONS="--max-old-space-size=4096" WORKDIR /myapp COPY --from=deps /myapp/node_modules /myapp/node_modules COPY --from=deps /myapp/apps /myapp/apps COPY --from=deps /myapp/packages /myapp/packages ADD package.json pnpm-lock.yaml pnpm-workspace.yaml ./ ADD prisma prisma ADD prisma.config.ts prisma.config.ts ADD tsconfig.json tsconfig.json ADD packages/db packages/db ADD . . RUN ./node_modules/.bin/prisma generate --config prisma.config.ts RUN node packages/db/scripts/build-runtime-artifacts.mjs # GeoIP-DB (AD-22): lokale, EU-konforme Land-Auflösung ins Image backen. Non-fatal — # schlägt der Download fehl, bleibt das Verzeichnis leer und die Auflösung ist zur Laufzeit ein Stub. RUN bash scripts/update-geoip-db.sh || echo "GeoIP-DB-Download übersprungen" # Reuse the already warm dependency store under one exclusive BuildKit mount. # The five disjoint closures deploy concurrently while the store lock is held. # Metadata filtering happens later through COPY --exclude, so the lock is not # extended by recursive tree walks and no inherited layer emits whiteouts. The # helper refreshes the store only after an offline cache miss. # /prod remains regular layer output and is independent of cache-mount GC. FROM build AS production-deps-bundle RUN --mount=type=cache,id=hivegrid-pnpm-store-v2,target=/pnpm/store,sharing=locked \ bash scripts/ci/deploy-production-closures.sh # Apply target-aware reachability pruning and describe each closure. Final # dependency COPY instructions omit source maps and declarations without # mutating the snapshotted closure trees. FROM production-deps-bundle AS web-production-deps RUN node /myapp/scripts/prune-prisma-client-runtime.mjs \ /prod/web/node_modules /myapp/packages/db/generated/prisma/client/internal/class.ts RUN SBOM_TARGET=web SBOM_ROOT_PATH=/prod/web SBOM_NODE_MODULES_PATH=/prod/web/node_modules \ SBOM_OUTPUT_PATH=/prod/web/sbom-pnpm.json node /myapp/scripts/generate-sbom.mjs FROM production-deps-bundle AS api-production-deps RUN node /myapp/scripts/prune-prisma-client-runtime.mjs \ /prod/api/node_modules /myapp/packages/db/generated/prisma/client/internal/class.ts RUN SBOM_TARGET=api SBOM_ROOT_PATH=/prod/api SBOM_NODE_MODULES_PATH=/prod/api/node_modules \ SBOM_OUTPUT_PATH=/prod/api/sbom-pnpm.json node /myapp/scripts/generate-sbom.mjs FROM production-deps-bundle AS mcp-production-deps RUN node /myapp/scripts/prune-prisma-client-runtime.mjs \ /prod/mcp/node_modules /myapp/packages/db/generated/prisma/client/internal/class.ts RUN SBOM_TARGET=mcp SBOM_ROOT_PATH=/prod/mcp SBOM_NODE_MODULES_PATH=/prod/mcp/node_modules \ SBOM_OUTPUT_PATH=/prod/mcp/sbom-pnpm.json node /myapp/scripts/generate-sbom.mjs FROM production-deps-bundle AS portal-domain-controller-production-deps RUN SBOM_TARGET=portal-domain-controller \ SBOM_ROOT_PATH=/prod/portal-domain-controller \ SBOM_NODE_MODULES_PATH=/prod/portal-domain-controller/node_modules \ SBOM_OUTPUT_PATH=/prod/portal-domain-controller/sbom-pnpm.json \ node /myapp/scripts/generate-sbom.mjs FROM production-deps-bundle AS worker-production-deps # Prisma CLI and TypeScript arrive only through optional peer resolution. The # worker uses the generated client at runtime and migrations run in CI, so # prune both roots and every virtual-store package reachable only through them. RUN node /myapp/scripts/prune-pnpm-runtime-closure.mjs \ /prod/worker/node_modules prisma typescript RUN node /myapp/scripts/prune-prisma-client-runtime.mjs \ /prod/worker/node_modules /myapp/packages/db/generated/prisma/client/internal/class.ts RUN SBOM_TARGET=worker SBOM_ROOT_PATH=/prod/worker SBOM_NODE_MODULES_PATH=/prod/worker/node_modules \ SBOM_OUTPUT_PATH=/prod/worker/sbom-pnpm.json node /myapp/scripts/generate-sbom.mjs FROM build AS web-build # VITE_* build args — Vite inlines these into the client bundle at build time. # Set as build-time env vars in Coolify (is_buildtime=true). Defaults are empty # so the build still succeeds when unset; client code falls back to no-op. ARG VITE_SENTRY_DSN_WEB="" ARG VITE_SENTRY_ENVIRONMENT="" ARG VITE_SENTRY_RELEASE="" ARG VITE_SENTRY_TRACES_SAMPLE_RATE="" ARG VITE_SENTRY_REPLAYS_SESSION_SAMPLE_RATE="" ARG VITE_SENTRY_REPLAYS_ON_ERROR_SAMPLE_RATE="" ARG VITE_OTEL_EXPORTER_OTLP_ENDPOINT="" ARG VITE_OTEL_PROPAGATE_API_HOST="" ARG VITE_OTEL_SERVICE_NAME="" ARG VITE_API_HOST="" ARG VITE_APP_REVISION="" ARG VITE_HOCUSPOCUS_URL="" ENV VITE_SENTRY_DSN_WEB="${VITE_SENTRY_DSN_WEB}" ENV VITE_SENTRY_ENVIRONMENT="${VITE_SENTRY_ENVIRONMENT}" ENV VITE_SENTRY_RELEASE="${VITE_SENTRY_RELEASE}" ENV VITE_SENTRY_TRACES_SAMPLE_RATE="${VITE_SENTRY_TRACES_SAMPLE_RATE}" ENV VITE_SENTRY_REPLAYS_SESSION_SAMPLE_RATE="${VITE_SENTRY_REPLAYS_SESSION_SAMPLE_RATE}" ENV VITE_SENTRY_REPLAYS_ON_ERROR_SAMPLE_RATE="${VITE_SENTRY_REPLAYS_ON_ERROR_SAMPLE_RATE}" ENV VITE_OTEL_EXPORTER_OTLP_ENDPOINT="${VITE_OTEL_EXPORTER_OTLP_ENDPOINT}" ENV VITE_OTEL_PROPAGATE_API_HOST="${VITE_OTEL_PROPAGATE_API_HOST}" ENV VITE_OTEL_SERVICE_NAME="${VITE_OTEL_SERVICE_NAME}" ENV VITE_API_HOST="${VITE_API_HOST}" ENV VITE_APP_REVISION="${VITE_APP_REVISION}" ENV VITE_HOCUSPOCUS_URL="${VITE_HOCUSPOCUS_URL}" RUN node scripts/build-web-surfaces.mjs && node scripts/build-web-server.mjs FROM build AS api-build RUN node apps/api/scripts/build.mjs FROM build AS mcp-build RUN node apps/mcp/scripts/build.mjs FROM build AS portal-domain-controller-build RUN cd apps/portal-domain-controller && node scripts/build.mjs FROM build AS worker-build RUN cd apps/worker && node scripts/build.mjs FROM web-build AS web-runtime-artifacts RUN find /myapp/build /myapp/apps/web/build /myapp/apps/web/build-portal -type f -name '*.map' -delete FROM api-build AS api-runtime-artifacts RUN find /myapp/apps/api/dist -type f -name '*.map' -delete FROM mcp-build AS mcp-runtime-artifacts RUN find /myapp/apps/mcp/dist -type f -name '*.map' -delete FROM portal-domain-controller-build AS portal-domain-controller-runtime-artifacts RUN find /myapp/apps/portal-domain-controller/dist -type f -name '*.map' -delete FROM worker-build AS worker-runtime-artifacts RUN find /myapp/apps/worker/dist -type f -name '*.map' -delete # Build the worker runtime image FROM base AS worker ARG APP_REVISION=unknown ENV NODE_ENV="production" ENV WORKER_ENABLED="true" # Default WORKER_QUEUE_GROUP for image users that don't set it explicitly. # In production each replica overrides this via docker-compose / K8s env to # one of {general, mail, import, webhook, plugin}. See docs/ops/worker-queues.md. ENV WORKER_QUEUE_GROUP="general" ENV WORKER_HEALTH_PORT="9100" ENV WORKER_ADMIN_PORT="9101" ENV APP_REVISION="${APP_REVISION}" LABEL org.opencontainers.image.revision="${APP_REVISION}" LABEL org.opencontainers.image.sbom="/myapp/sbom-pnpm.json" WORKDIR /myapp COPY --exclude=**/*.map --exclude=**/*.d.ts --from=worker-production-deps /prod/worker/node_modules /myapp/node_modules COPY --from=worker-production-deps /prod/worker/sbom-pnpm.json /myapp/sbom-pnpm.json COPY --from=worker-runtime-artifacts /myapp/apps/worker/dist /myapp/apps/worker/dist RUN printf '%s\n' "$APP_REVISION" > /myapp/.image-revision # Worker runtime does not install, build, or migrate packages. RUN rm -rf /root/.cache/node/corepack /usr/local/lib/node_modules/corepack /opt/yarn-* \ /usr/local/bin/corepack /usr/local/bin/pnpm /usr/local/bin/pnpx \ /usr/local/bin/yarn /usr/local/bin/yarnpkg USER node HEALTHCHECK --interval=30s --timeout=10s --start-period=20s --start-interval=2s --retries=3 \ CMD node apps/worker/dist/health.js CMD ["node", "apps/worker/dist/index.js"] # Private controller for customer-portal custom-domain ingress. This image has # no database, Coolify, SSH, Cloudflare, root, or Docker-socket capability. FROM base AS portal-domain-controller # The controller's authoritative filesystem fence uses the kernel-backed # /usr/bin/flock binary. Install it explicitly instead of relying on an # incidental package in the pinned slim base image. RUN apt-get update \ && apt-get install -y --no-install-recommends util-linux \ && rm -rf /var/lib/apt/lists/* ARG APP_REVISION=unknown ENV NODE_ENV="production" ENV PORT="8080" ENV PORTAL_DOMAIN_CONTROLLER_PORT="8080" ENV PORTAL_DOMAIN_CONTROLLER_CONFIG_DIR="/data/coolify/proxy/dynamic/hivegrid-portal-domains" ENV PORTAL_DOMAIN_CONTROLLER_RELOAD_TRIGGER_FILE="/data/coolify/proxy/dynamic/.hivegrid-portal-domains.reload" ENV APP_REVISION="${APP_REVISION}" LABEL org.opencontainers.image.revision="${APP_REVISION}" LABEL org.opencontainers.image.sbom="/myapp/sbom-pnpm.json" EXPOSE 8080 WORKDIR /myapp COPY --exclude=**/*.map --exclude=**/*.d.ts --from=portal-domain-controller-production-deps /prod/portal-domain-controller/node_modules /myapp/node_modules COPY --from=portal-domain-controller-production-deps /prod/portal-domain-controller/sbom-pnpm.json /myapp/sbom-pnpm.json COPY --from=portal-domain-controller-runtime-artifacts /myapp/apps/portal-domain-controller/dist /myapp/apps/portal-domain-controller/dist RUN printf '%s\n' "$APP_REVISION" > /myapp/.image-revision \ && mkdir -p /data/coolify/proxy/dynamic/hivegrid-portal-domains \ && chown node:node /data/coolify/proxy/dynamic/hivegrid-portal-domains \ && rm -rf /root/.cache/node/corepack /usr/local/lib/node_modules/corepack /opt/yarn-* \ /usr/local/bin/corepack /usr/local/bin/pnpm /usr/local/bin/pnpx \ /usr/local/bin/yarn /usr/local/bin/yarnpkg USER node HEALTHCHECK --interval=30s --timeout=10s --start-period=20s --start-interval=2s --retries=3 \ CMD node apps/portal-domain-controller/dist/deploy-health.js "$APP_REVISION" CMD ["node", "apps/portal-domain-controller/dist/index.js"] # Build the API runtime image FROM base AS api ARG APP_REVISION=unknown ENV PORT="8080" ENV NODE_ENV="production" ENV APP_REVISION="${APP_REVISION}" LABEL org.opencontainers.image.revision="${APP_REVISION}" LABEL org.opencontainers.image.sbom="/myapp/sbom-pnpm.json" EXPOSE 8080 WORKDIR /myapp COPY --exclude=**/*.map --exclude=**/*.d.ts --from=api-production-deps /prod/api/node_modules /myapp/node_modules COPY --from=api-production-deps /prod/api/sbom-pnpm.json /myapp/sbom-pnpm.json COPY --from=api-runtime-artifacts /myapp/apps/api/dist /myapp/apps/api/dist # API runtime does not install, build, or migrate packages. Remove package # manager shims inherited from the build base to reduce executable surface. RUN rm -rf /root/.cache/node/corepack /usr/local/lib/node_modules/corepack /opt/yarn-* \ /usr/local/bin/corepack /usr/local/bin/pnpm /usr/local/bin/pnpx \ /usr/local/bin/yarn /usr/local/bin/yarnpkg USER node HEALTHCHECK --interval=30s --timeout=10s --start-period=20s --start-interval=2s --retries=3 \ CMD node -e "fetch('http://127.0.0.1:${API_PORT:-${PORT}}/healthz').then((response) => { if (!response.ok) process.exit(1); }).catch(() => process.exit(1))" CMD ["node", "apps/api/dist/server.js"] # Build the MCP runtime image FROM base AS mcp ARG APP_REVISION=unknown ENV PORT="8080" ENV MCP_PORT="8080" ENV NODE_ENV="production" ENV APP_REVISION="${APP_REVISION}" LABEL org.opencontainers.image.revision="${APP_REVISION}" LABEL org.opencontainers.image.sbom="/myapp/sbom-pnpm.json" EXPOSE 8080 WORKDIR /myapp COPY --exclude=**/*.map --exclude=**/*.d.ts --from=mcp-production-deps /prod/mcp/node_modules /myapp/node_modules COPY --from=mcp-production-deps /prod/mcp/sbom-pnpm.json /myapp/sbom-pnpm.json COPY --from=mcp-runtime-artifacts /myapp/apps/mcp/dist /myapp/apps/mcp/dist RUN printf '%s\n' "$APP_REVISION" > /myapp/.image-revision # MCP runtime does not install or build packages. Keep package managers out of # the remotely reachable service image. RUN rm -rf /root/.cache/node/corepack /usr/local/lib/node_modules/corepack /opt/yarn-* \ /usr/local/bin/corepack /usr/local/bin/pnpm /usr/local/bin/pnpx \ /usr/local/bin/yarn /usr/local/bin/yarnpkg USER node HEALTHCHECK --interval=30s --timeout=10s --start-period=20s --start-interval=2s --retries=3 \ CMD node -e "fetch('http://127.0.0.1:${MCP_PORT:-${PORT}}/healthz').then((response) => { if (!response.ok) process.exit(1); }).catch(() => process.exit(1))" CMD ["node", "apps/mcp/dist/server.js"] # Finally, build the production web image with minimal footprint FROM base AS web ARG APP_REVISION=unknown ENV PORT="8080" ENV NODE_ENV="production" ENV APP_REVISION="${APP_REVISION}" LABEL org.opencontainers.image.revision="${APP_REVISION}" LABEL org.opencontainers.image.sbom="/myapp/sbom-pnpm.json" EXPOSE 8080 WORKDIR /myapp COPY --exclude=**/*.map --exclude=**/*.d.ts --from=web-production-deps /prod/web/node_modules /myapp/node_modules COPY --from=web-runtime-artifacts /myapp/build /myapp/build COPY --from=web-runtime-artifacts /myapp/build/server-runtime /myapp/build/server-runtime COPY --from=build /myapp/pnpm-workspace.yaml /myapp/pnpm-workspace.yaml COPY --from=web-production-deps /prod/web/sbom-pnpm.json /myapp/sbom-pnpm.json COPY --from=web-runtime-artifacts /myapp/apps/web/build /myapp/apps/web/build COPY --from=web-runtime-artifacts /myapp/apps/web/build-portal /myapp/apps/web/build-portal COPY --from=build /myapp/apps/web/public /myapp/apps/web/public COPY --from=build /myapp/package.json /myapp/package.json COPY --from=build /myapp/start.sh /myapp/start.sh COPY --from=build /myapp/scripts/verify-migrator-role.mjs /myapp/scripts/verify-migrator-role.mjs COPY --from=build /myapp/scripts/adopt-prisma-baseline.mjs /myapp/scripts/adopt-prisma-baseline.mjs COPY --from=build /myapp/packages/db/dist /myapp/packages/db/dist COPY --from=build /myapp/prisma /myapp/prisma COPY --from=build /myapp/prisma.config.ts /myapp/prisma.config.ts # GeoIP-DB (AD-22) ins Web-Runtime-Image; absoluter Pfad gegen cwd-Ambiguität. COPY --from=build /myapp/data/geoip /myapp/data/geoip ENV GEOIP_DB_PATH="/myapp/data/geoip/dbip-country-lite.mmdb" # The entrypoint invokes the already-installed Prisma CLI directly. Package # managers are build tools and must not download or mutate dependencies while # the non-root production container is starting. RUN chmod +x /myapp/start.sh \ && rm -rf /root/.cache/node/corepack /usr/local/lib/node_modules/corepack /opt/yarn-* \ /usr/local/bin/corepack /usr/local/bin/pnpm /usr/local/bin/pnpx \ /usr/local/bin/yarn /usr/local/bin/yarnpkg USER node # Keep the same image and probe for all isolated web surfaces. The healthcheck # selects the canonical host from the fail-closed runtime surface contract. HEALTHCHECK --interval=30s --timeout=10s --start-period=120s --start-interval=2s --retries=3 \ CMD node -e "const http=require('node:http');const surface=process.env.WEB_RUNTIME_SURFACE;if(surface==='portal')process.env.WEB_APP_HOST=process.env.PORTAL_APP_HOST;else if(surface==='admin')process.env.WEB_APP_HOST=process.env.ADMIN_APP_HOST;else if(surface!=='core')process.exit(1);if(!process.env.WEB_APP_HOST)process.exit(1);const request=http.get({hostname:'127.0.0.1',port:process.env.PORT,path:'/healthz',headers:{host:process.env.WEB_APP_HOST}},(response)=>{response.on('end',()=>process.exit(response.statusCode===200?0:1));response.resume()});request.on('error',()=>process.exit(1));request.setTimeout(9000,()=>request.destroy(new Error('healthcheck timeout')))" ENTRYPOINT [ "./start.sh" ]