# ───────────────────────────────────────────────────────────────────────────── # Stage 1 — npm install (Tailwind CLI) # Uses node:24-alpine for a small image. npm ci is used for reproducible, # fast installs from the lockfile. # ───────────────────────────────────────────────────────────────────────────── FROM node:24-alpine AS npm-install WORKDIR /npm COPY Htmx.ApiDemo/package.json Htmx.ApiDemo/package-lock.json* ./ RUN npm ci # ───────────────────────────────────────────────────────────────────────────── # Stage 2 — AOT publish # Uses the Alpine SDK image so the binary is linked against musl libc, # making it compatible with the Alpine runtime image in Stage 3. # # Alpine packages are cached via BuildKit mount cache — after the first build # `apk add` is near-instant because the package index and downloads are # reused from the local cache rather than re-fetched from the network. # ───────────────────────────────────────────────────────────────────────────── FROM mcr.microsoft.com/dotnet/sdk:10.0-alpine AS publish # Install AOT linker tools and Node.js (for the Tailwind MSBuild target). # --mount=type=cache keeps the apk package cache between builds on this machine. RUN --mount=type=cache,target=/var/cache/apk \ apk add --no-cache clang lld musl-dev build-base nodejs npm WORKDIR /src # Copy project files first — NuGet restore layer is cached until these change. 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 -r linux-musl-x64 # Bring in pre-installed node_modules from Stage 1. COPY --from=npm-install /npm/node_modules Htmx.ApiDemo/node_modules # Copy the rest of the source COPY . . # AOT publish targeting musl so the binary runs on Alpine. RUN dotnet publish Htmx.ApiDemo/Htmx.ApiDemo.csproj \ -c Release \ -r linux-musl-x64 \ --no-restore \ --self-contained true \ -o /publish # ───────────────────────────────────────────────────────────────────────────── # Stage 3 — Runtime image (~12 MB base vs ~100 MB on Debian) # runtime-deps:alpine provides only the native libs the AOT binary needs. # No .NET runtime is included — the binary is fully self-contained. # ───────────────────────────────────────────────────────────────────────────── FROM mcr.microsoft.com/dotnet/runtime-deps:10.0-alpine AS runtime WORKDIR /app COPY --from=publish /publish . # Cloud Run injects PORT (default 8080). # ASP.NET Core reads ASPNETCORE_HTTP_PORTS directly — no entrypoint script needed. ENV ASPNETCORE_HTTP_PORTS=8080 # Use the built-in non-root user provided by the official .NET Alpine image. USER $APP_UID EXPOSE 8080 ENTRYPOINT ["./Htmx.ApiDemo"]