FROM node:22-alpine AS base WORKDIR /app ENV TURBO_TELEMETRY_DISABLED=1 ENV PNPM_HOME="/pnpm" ENV PATH="$PNPM_HOME:$PNPM_HOME/bin:$PATH" ENV CI=1 RUN corepack enable pnpm # =========================================================================== # FROM base AS builder RUN pnpm add -g turbo@2.9.18 COPY . . # Create a pruned workspace for just the space app RUN turbo prune --scope=space --docker # =========================================================================== # FROM base AS installer # Build in production mode; we still install dev deps explicitly below ENV NODE_ENV=production # Public envs required at build time (pick up via process.env) ARG VITE_API_BASE_URL="" ENV VITE_API_BASE_URL=$VITE_API_BASE_URL ARG VITE_API_BASE_PATH="/api" ENV VITE_API_BASE_PATH=$VITE_API_BASE_PATH ARG VITE_ADMIN_BASE_URL="" ENV VITE_ADMIN_BASE_URL=$VITE_ADMIN_BASE_URL ARG VITE_ADMIN_BASE_PATH="/god-mode" ENV VITE_ADMIN_BASE_PATH=$VITE_ADMIN_BASE_PATH ARG VITE_SPACE_BASE_URL="" ENV VITE_SPACE_BASE_URL=$VITE_SPACE_BASE_URL ARG VITE_SPACE_BASE_PATH="/spaces" ENV VITE_SPACE_BASE_PATH=$VITE_SPACE_BASE_PATH ARG VITE_LIVE_BASE_URL="" ENV VITE_LIVE_BASE_URL=$VITE_LIVE_BASE_URL ARG VITE_LIVE_BASE_PATH="/live" ENV VITE_LIVE_BASE_PATH=$VITE_LIVE_BASE_PATH ARG VITE_WEB_BASE_URL="" ENV VITE_WEB_BASE_URL=$VITE_WEB_BASE_URL ARG VITE_WEB_BASE_PATH="" ENV VITE_WEB_BASE_PATH=$VITE_WEB_BASE_PATH ARG VITE_WEBSITE_URL="https://plane.so" ENV VITE_WEBSITE_URL=$VITE_WEBSITE_URL ARG VITE_SUPPORT_EMAIL="support@plane.so" ENV VITE_SUPPORT_EMAIL=$VITE_SUPPORT_EMAIL COPY .gitignore .gitignore COPY --from=builder /app/out/json/ . COPY --from=builder /app/out/pnpm-lock.yaml ./pnpm-lock.yaml # Copy full directory structure before fetch to ensure all package.json files are available COPY --from=builder /app/out/full/ . COPY turbo.json turbo.json # Fetch dependencies to cache store, then install offline with dev deps RUN --mount=type=cache,id=pnpm-store,target=/pnpm/store pnpm fetch --store-dir=/pnpm/store RUN --mount=type=cache,id=pnpm-store,target=/pnpm/store CI=true pnpm install --offline --frozen-lockfile --store-dir=/pnpm/store --prod=false # Build only the space package RUN pnpm turbo run build --filter=space # =========================================================================== # FROM base AS runner ENV NODE_ENV=production # OS security updates. --available forces reinstall from the current repos; # bump APK_SECURITY_PATCH to bust buildx's cached layer (keyed on this command # string), otherwise a rebuild silently re-ships the old packages. ARG APK_SECURITY_PATCH=2026-09-07 RUN echo "apk-security-patch ${APK_SECURITY_PATCH}" && apk update && apk upgrade --no-cache --available # Remove go from Alpine APK database; not needed at runtime and carries stdlib CVEs RUN apk del go 2>/dev/null || true # npm is not used at runtime (the CMD execs the react-router-serve bin shim # directly rather than going through npx); its bundled dependencies carry known # vulnerabilities (tar, brace-expansion, pacote, sigstore, ip-address, fast-uri, # browserslist), so remove the whole CLI rather than picking off one package at # a time. RUN rm -rf /usr/local/lib/node_modules/npm /usr/local/bin/npm /usr/local/bin/npx COPY --from=installer /app/apps/space/build ./apps/space/build COPY --from=installer /app/apps/space/node_modules ./apps/space/node_modules COPY --from=installer /app/node_modules ./node_modules # esbuild and tsgolint are build-only Go binaries; remove from runtime image to eliminate stdlib CVEs RUN find /app/node_modules \( -name 'esbuild' -o -name 'tsgolint' \) -type f -delete 2>/dev/null || true WORKDIR /app/apps/space EXPOSE 3000 RUN apk add --no-cache curl HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ CMD curl -fsS http://127.0.0.1:3000/spaces/ >/dev/null || exit 1 CMD ["./node_modules/.bin/react-router-serve", "./build/server/index.js"]