# ───────────────────────────────────────────────────────────────────────────── # Stage 1 — npm install (Tailwind CLI) # The .NET SDK stage needs node_modules present before running dotnet publish # because the MSBuild Tailwind target calls `npx @tailwindcss/cli` during build. # # We use the official node:24-slim image here. This means the npm that ships # with Node 24 (npm 10.x) is used as-is — we deliberately do NOT run # `npm install -g npm@latest` anywhere. Running a global npm self-upgrade # inside a Debian container is a known reliability hazard: npm replaces its # own running binaries mid-flight, which can cause EBUSY / ENOENT failures # that corrupt the install. The bundled npm is current enough. # ───────────────────────────────────────────────────────────────────────────── FROM node:24-slim AS npm-install WORKDIR /npm COPY Htmx.ApiDemo/package.json . # npm ci requires package-lock.json; if it doesn't exist yet, run # `npm install` locally first to generate it, then commit it to the repo. COPY Htmx.ApiDemo/package-lock.json* ./ # ci is preferred over install in CI/Docker contexts: respects package-lock, # clean installs, and is faster. RUN npm ci # ───────────────────────────────────────────────────────────────────────────── # Stage 2 — AOT publish # Uses the full .NET 10 SDK image. Node/npx must also be present here so the # Tailwind MSBuild target can run. We install Node 24 from NodeSource using # the official setup script and then immediately install nodejs via apt — no # subsequent `npm install -g npm` step, for the same reason as above. # ───────────────────────────────────────────────────────────────────────────── FROM mcr.microsoft.com/dotnet/sdk:10.0 AS publish # Install Node.js 24 (required by the Tailwind MSBuild target at publish time). # We download the NodeSource setup script to a file first so we can inspect it # if needed, then run it. Using `| bash -` directly is convenient but hides # the script from audit — the two-step form is safer in CI/CD contexts. RUN apt-get update && \ apt-get install -y --no-install-recommends curl ca-certificates && \ curl -fsSL https://deb.nodesource.com/setup_24.x -o /tmp/nodesource_setup.sh && \ bash /tmp/nodesource_setup.sh && \ apt-get install -y --no-install-recommends nodejs && \ rm /tmp/nodesource_setup.sh && \ rm -rf /var/lib/apt/lists/* # Intentionally no `npm install -g npm` — see Stage 1 note above. WORKDIR /src # Copy solution and project files first so NuGet restore is cached separately COPY Htmx.slnx . COPY Htmx.ApiDemo/Htmx.ApiDemo.csproj Htmx.ApiDemo/ COPY Htmx.SourceGenerator/Htmx.SourceGenerator.csproj Htmx.SourceGenerator/ RUN dotnet restore Htmx.ApiDemo/Htmx.ApiDemo.csproj # Bring in the pre-installed node_modules from Stage 1. # These were installed with `npm ci` on Node 24 — no npm upgrade was performed. COPY --from=npm-install /npm/node_modules Htmx.ApiDemo/node_modules # Copy the rest of the source COPY . . # AOT publish — output goes to /publish RUN dotnet publish Htmx.ApiDemo/Htmx.ApiDemo.csproj \ -c Release \ --no-restore \ -o /publish # ───────────────────────────────────────────────────────────────────────────── # Stage 3 — Runtime image # runtime-deps provides the native library dependencies (libc, libssl, libicu) # that the AOT binary links against at runtime — no .NET runtime needed. # ───────────────────────────────────────────────────────────────────────────── FROM mcr.microsoft.com/dotnet/runtime-deps:10.0 AS runtime # Run as non-root for security hardening (recommended by Cloud Run docs) RUN addgroup --system appgroup && adduser --system --ingroup appgroup appuser WORKDIR /app COPY --from=publish /publish . # Ensure the binary is executable RUN chmod +x ./Htmx.ApiDemo # Cloud Run injects PORT (default 8080). # ASP.NET Core reads ASPNETCORE_HTTP_PORTS, not PORT directly, so we set it. # The entrypoint script below maps $PORT → ASPNETCORE_HTTP_PORTS at startup. COPY GCR/entrypoint.sh /entrypoint.sh RUN chmod +x /entrypoint.sh # Transfer ownership so the app can write temp files if needed RUN chown -R appuser:appgroup /app USER appuser EXPOSE 8080 ENTRYPOINT ["/entrypoint.sh"]