Blazor Server is unusual among web app architectures: the UI lives on the server, and the browser maintains a persistent SignalR connection (a "circuit") that streams DOM diffs back and forth. That model has real deployment implications. You can't treat it like a stateless API, because each user's UI state is held in server memory and tied to a live WebSocket. Get the transport and session affinity right and Blazor Server is a joy to run; get them wrong and you'll see circuits dropping and full-page reloads.
Understand the circuit
When a user loads a Blazor Server page, the server establishes a SignalR circuit. Every click, input, and render happens over that connection. Two things follow:
- 1WebSockets are strongly preferred. SignalR can fall back to long polling, but that's far less efficient and adds latency to every interaction. Your ingress must allow WebSocket upgrades.
- 2Affinity matters at scale. If you run multiple replicas, a reconnecting client should land on the same instance or it loses its circuit state. For a single replica this is moot; for scaled-out deployments, you either configure sticky sessions or use a backplane.
Prepare the app
Make your app read the port from the environment and listen on all interfaces. In Program.cs this is usually handled by the ASPNETCORE_URLS environment variable:
ASPNETCORE_URLS=http://0.0.0.0:8080Add EF Core and read the connection string from configuration, which ASP.NET maps from environment variables automatically:
builder.Services.AddDbContext<AppDbContext>(opt =>
opt.UseNpgsql(builder.Configuration.GetConnectionString("Default")
?? Environment.GetEnvironmentVariable("DATABASE_URL")));Note that Npgsql expects a key-value connection string (Host=...;Username=...;Password=...;Database=...) rather than a URI. If your platform injects a postgresql:// URL, parse it into the Npgsql format at startup, or set the discrete connection-string keys as environment variables.
Add a health check endpoint:
builder.Services.AddHealthChecks().AddNpgSql(connStr);
app.MapHealthChecks("/health");Dockerfile
Use the official .NET multi-stage pattern. Publish a trimmed, framework-dependent build for fast deploys:
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY . .
RUN dotnet restore
RUN dotnet publish -c Release -o /app --no-restore
FROM mcr.microsoft.com/dotnet/aspnet:8.0
WORKDIR /app
COPY --from=build /app .
ENV ASPNETCORE_URLS=http://0.0.0.0:8080
EXPOSE 8080
ENTRYPOINT ["dotnet", "YourApp.dll"]The aspnet runtime image is smaller than the SDK and contains exactly what a published app needs.
Deploying on PandaStack
With the Dockerfile in your repo:
- 1Connect the GitHub repo as a container app.
- 2PandaStack builds with rootless BuildKit in an ephemeral Job pod, pushes to Artifact Registry, and deploys with Helm. Kong ingress handles routing.
- 3Provision a managed PostgreSQL database;
DATABASE_URLis injected. Add discrete Npgsql connection-string keys if you prefer the explicit format. - 4Run EF Core migrations. The cleanest approach is to apply migrations at startup for small apps, or run them as a one-off:
using (var scope = app.Services.CreateScope())
{
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
db.Database.Migrate();
}WebSocket and circuit considerations
Kong ingress supports WebSocket upgrades, which is what the SignalR circuit needs. Configure the SignalR circuit timeouts to be tolerant of brief network blips so a user who switches networks can reconnect:
builder.Services.AddServerSideBlazor().AddHubOptions(o =>
{
o.ClientTimeoutInterval = TimeSpan.FromSeconds(60);
o.KeepAliveInterval = TimeSpan.FromSeconds(15);
});| Consideration | Blazor Server guidance |
|---|---|
| Transport | WebSocket required for good UX |
| Replicas | Single replica avoids affinity issues; scale-out needs sticky sessions |
| Memory | Each circuit holds UI state; size memory tier accordingly |
| Cold start | Avoid scale-to-zero for latency-sensitive interactive apps |
A caution about the free tier: scale-to-zero on spot nodes means a cold start after idle, and any active circuits are lost when the app scales down. For a low-traffic internal tool that's acceptable. For a customer-facing interactive app, run on a paid tier with a warm instance and ideally a single replica or sticky sessions, since Blazor Server's stateful nature doesn't play well with cold starts.
Verifying
Load the app over its automatic-SSL domain and open the browser dev tools network tab; you should see a WebSocket connection to the SignalR hub stay open. Interactions should feel instant with no full-page reloads. Use the server-side metrics view to watch memory, since circuit state accumulates with concurrent users.
Common pitfalls
- Connection string format: Npgsql wants key-value, not a URI.
- WebSockets blocked: if the circuit can't use WebSockets it falls back to polling and feels sluggish.
- Scaling without affinity: reconnecting users lose their circuit and get a reload.
- Memory growth: many concurrent circuits consume server memory; choose a memory-optimized tier for heavy interactive use.
References
- Blazor Server hosting and deployment: https://learn.microsoft.com/aspnet/core/blazor/host-and-deploy/server
- SignalR configuration: https://learn.microsoft.com/aspnet/core/signalr/configuration
- EF Core migrations: https://learn.microsoft.com/ef/core/managing-schemas/migrations/
- Npgsql connection string parameters: https://www.npgsql.org/doc/connection-string-parameters.html
- .NET container images: https://learn.microsoft.com/dotnet/architecture/microservices/net-core-net-framework-containers/official-net-docker-images
Blazor Server's stateful circuit makes it different from a typical web deploy, but with WebSockets and the right replica strategy it runs cleanly in containers. Spin up a container app plus a managed PostgreSQL database on PandaStack's free tier to try it: https://dashboard.pandastack.io