Dockerizing .NET 8 Blazor Web App with a Single Container

Summary

The challenge involves containerizing a modern Blazor Web App (introduced in .NET 8), which utilizes a split architecture consisting of a Server project and a Client project. The core misconception is treating these as two independent, decoupled services that need separate Docker containers. In a production Blazor Web App environment, the Client (WebAssembly) assets are served by the Server project during the rendering process. Attempting to split them into two separate Docker services via docker-compose introduces unnecessary network complexity and breaks the Server-Side Rendering (SSR) lifecycle.

Root Cause

The architectural misunderstanding stems from a confusion between Blazor WebAssembly (Standalone) and the Blazor Web App (Unified) model:

  • Unified Asset Management: In the Blazor Web App template, the Server project acts as the host. It manages the routing, handles SSR, and serves the static files required for the Client (WASM) components.
  • Incorrect Decoupling: Thinking the Client subproject needs its own service ignores that the Client code is compiled into static assets (WASM binaries, JS, CSS) that the Server must deliver to the browser.
  • Complexity Overload: Trying to orchestrate two containers for one application increases latency, complicates CORS (Cross-Origin Resource Sharing) configurations, and breaks the seamless state transfer required for Pre-rendering.

Why This Happens in Real Systems

In distributed systems, engineers often fall into the trap of “Microservice Fever,” where every logical separation is treated as a physical separation.

  • Mental Model Mismatch: Developers used to SPA frameworks (like React + Node) are accustomed to separating the frontend build from the backend API.
  • Dev vs. Prod Divergence: Local development uses dotnet watch which simulates a highly dynamic environment. When moving to Docker, engineers try to replicate that “split” feel rather than following the compiled deployment pattern.
  • Abstraction Leaks: The complexity of the .NET 8 render modes (Static, InteractiveServer, InteractiveWebAssembly) is often not fully understood, leading to incorrect assumptions about how the components are actually executed.

Real-World Impact

  • Increased Latency: If the Client assets are served from a separate container, every initial page load requires an extra network hop or complex proxying.
  • Broken Pre-rendering: The primary benefit of Blazor Web App is SSR. If the server cannot immediately access the client assets to pre-render the UI, the user sees a “flicker” or a blank screen while WASM initializes.
  • Operational Overhead: Managing two Docker images, two deployment pipelines, and a docker-compose file for what is essentially a single unit of deployment increases the Total Cost of Ownership (TCO).
  • CORS Issues: Moving the client to a separate service necessitates complex CORS policies, which are a frequent source of production outages.

Example or Code

The correct approach is a Multi-stage Dockerfile that builds both projects but produces a single, runnable artifact from the Server project.

FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src

# Copy csproj files and restore as distinct layers to leverage Docker cache
COPY ["Server/Server.csproj", "Server/"]
COPY ["Client/Client.csproj", "Client/"]
RUN dotnet restore "Server/Server.csproj"

# Copy the entire source and build the Server project
COPY . .
WORKDIR "/src/Server"
RUN dotnet build "Server.csproj" -c Release -o /app/build

FROM build AS publish
RUN dotnet publish "Server.csproj" -c Release -o /app/publish /p:UseAppHost=false

FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS final
WORKDIR /app
EXPOSE 80
EXPOSE 443
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "Server.dll"]

How Senior Engineers Fix It

Senior engineers focus on the Deployment Unit. They recognize that while the code is logically separated into subprojects, the runtime unit is singular.

  • Single Container Pattern: They build a single Docker image where the Server project contains the compiled WebAssembly assets in its wwwroot folder.
  • Multi-Stage Builds: They use multi-stage Dockerfiles to ensure the final image is lightweight, containing only the aspnet runtime and the published binaries, excluding the heavy sdk.
  • CI/CD Optimization: They structure the build process to cache the dotnet restore layer, significantly speeding up deployment pipelines.
  • Simplified Orchestration: Instead of a complex docker-compose.yaml, they deploy a single service, reducing the surface area for configuration errors.

Why Juniors Miss It

  • Over-engineering: Juniors often try to solve the “separation of concerns” by physically separating the containers, not realizing that logical separation $\neq$ physical separation.
  • Focusing on the “How” rather than the “Why”: They focus on how to make two containers talk to each other rather than asking why they need two containers in the first place.
  • Misinterpreting Project Structure: They see two folders (Client and Server) and instinctively assume they represent two independent running processes.
  • Lack of Runtime Awareness: They often don’t understand the lifecycle of a request in an SSR-enabled application, specifically how the server must “know” the client’s state to perform pre-rendering.

Leave a Comment