When I first started containerizing, I had no clue what I was doing. Docker? Sounded like a shipping company to me. But man, once I got the hang of it—those late-night battles with YAML files, the victory dance when an ASP.NET Core API finally ran in a container—it changed everything. Blazor apps, APIs, you name it; I’ve stuffed ’em all into Docker images. So, if you’re tired of ‘works on my machine’ chaos, grab a coffee. Let’s break down Docker deployment without the textbook fluff.
![Docker Deployment for ASP.NET Core API & Blazor Apps]()
Introduction
Imagine writing your application once and knowing that it will run exactly the same way on your development laptop, on your colleague’s machine, and even in production. That’s one of the strongest appeals of Docker. Whether you’re building an ASP.NET Core API serving as a backend for a mobile app or a modern Blazor web app that delivers a dynamic user interface, Docker can streamline your development and deployment process.
In this article, we’ll cover the basics of Docker deployment in the context of ASP.NET Core APIs and Blazor apps. I’ll walk you through setting up your project structure, writing Dockerfiles, using Docker Compose for multi-container orchestration, and sharing some personal anecdotes along the way. Even if you’re a beginner, the step-by-step instructions will help you build a solid foundation in containerization.
Getting to Know Docker
At its core, Docker is a container platform that packages your application code, runtime, libraries, and dependencies into a neat, portable unit called a container. Containers ensure that your application behaves consistently regardless of where it is deployed.
Initially, I was fascinated by the concept. I remember the first time I built a simple containerized ASP.NET Core app and watched it run identically on a cloud server as it did on my local machine. That “aha” moment made me realize how Docker could solve countless “works on my machine” issues.
For ASP.NET Core and Blazor developers, Docker provides excellent benefits
- Consistency: Always run the same environment.
- Scalability: Easily scale your services in production.
- Isolation: Each service runs in its own container, reducing conflicts.
- Efficiency: Minimal overhead compared to traditional virtual machines.
Project Structure Overview
Let’s imagine you’re working on a project called "MicroserviceProject" with two main components:
- WeatherApi: An ASP.NET Core Web API that returns weather data.
- WeatherApp: A Blazor application that consumes the API and displays the data interactively.
WeatherApi: WeatherForecastController.cs - API
using Microsoft.AspNetCore.Mvc;
namespace WeatherApi.Controllers;
[ApiController]
[Route("api/[controller]")]
public class WeatherForecastController : ControllerBase
{
private static readonly string[] Summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
[HttpGet]
public IEnumerable<WeatherForecast> Get()
{
return Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = DateTime.Now.AddDays(index),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = Summaries[Random.Shared.Next(Summaries.Length)]
})
.ToArray();
}
public class WeatherForecast
{
public DateTime Date { get; set; }
public int TemperatureC { get; set; }
public string? Summary { get; set; }
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}
}
WeatherApp
Weathers.razor is a Blazor component that retrieves weather forecast data via an HTTP request to an API and displays it in a neatly formatted table.
@page "/weathers"
@inject HttpClient Http
<h3>Weather Forecast</h3>
@if (forecasts == null) {
<p><em>Loading...</em></p>
} else {
<table class="table">
<thead>
<tr>
<th>Date</th>
<th>Temp. (C)</th>
<th>Temp. (F)</th>
<th>Summary</th>
</tr>
</thead>
<tbody>
@foreach (var forecast in forecasts) {
<tr>
<td>@forecast.Date.ToShortDateString()</td>
<td>@forecast.TemperatureC</td>
<td>@forecast.TemperatureF</td>
<td>@forecast.Summary</td>
</tr>
}
</tbody>
</table>
}
@code {
private WeatherForecast[]? forecasts;
protected override async Task OnInitializedAsync() {
forecasts = await Http.GetFromJsonAsync<WeatherForecast[]>("http://192.168.0.104:5000/api/weatherforecast");
}
public class WeatherForecast {
public DateTime Date { get; set; }
public int TemperatureC { get; set; }
public string? Summary { get; set; }
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}
}
Weather.razor is a Blazor component that asynchronously loads and renders simulated weather forecast data in a streamlined, user-friendly table using stream rendering.
@page "/weather"
@attribute [StreamRendering]
<h1>Weather</h1>
<p>This component demonstrates showing data.</p>
@if (forecasts == null)
{
<p><em>Loading...</em></p>
}
else
{
<table class="table">
<thead>
<tr>
<th>Date</th>
<th>Temp. (C)</th>
<th>Temp. (F)</th>
<th>Summary</th>
</tr>
</thead>
<tbody>
@foreach (var forecast in forecasts)
{
<tr>
<td>@forecast.Date.ToShortDateString()</td>
<td>@forecast.TemperatureC</td>
<td>@forecast.TemperatureF</td>
<td>@forecast.Summary</td>
</tr>
}
</tbody>
</table>
}
@code {
private WeatherForecast[]? forecasts;
protected override async Task OnInitializedAsync()
{
// Simulate asynchronous loading to demonstrate streaming rendering
await Task.Delay(500);
var startDate = DateOnly.FromDateTime(DateTime.Now);
var summaries = new[] { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" };
forecasts = Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = startDate.AddDays(index),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = summaries[Random.Shared.Next(summaries.Length)]
}).ToArray();
}
private class WeatherForecast
{
public DateOnly Date { get; set; }
public int TemperatureC { get; set; }
public string? Summary { get; set; }
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}
}
Your directory might look something like this
d:\MicroserviceProject
│
├── docker-compose.yml
│
├── WeatherApi
│ ├── appsettings.Development.json
│ ├── appsettings.json
│ ├── Dockerfile
│ ├── Program.cs
│ ├── WeatherApi.csproj
│ ├── WeatherApi.http
│ ├── Controllers
│ │ └── WeatherForecastController.cs
│ ├── Properties
│ │ └── (various property files)
│ └── obj
│ └── (build/generated files, project.assets.json, *.nuget.* files, etc.)
│
└── WeatherApp
├── appsettings.Development.json
├── appsettings.json
├── Dockerfile
├── Program.cs
├── WeatherApp.csproj
├── Components
│ ├── _Imports.razor
│ ├── App.razor
│ ├── Routes.razor
│ ├── Page (folder with all Page)
│ └── Layout (folder with layout files)
├── Properties
│ └── launchSettings.json
├── Shared
│ └── NavMenu.razor
├── wwwroot
│ └── (static assets)
└── obj
└── (build-generated files, project.assets.json, *.nuget.* files, etc.)
Getting Started: Building Docker Images
The first step in Docker deployment is writing Dockerfiles for both the ASP.NET Core API and the Blazor app. Let’s break down the process.
Writing a Dockerfile for ASP.NET Core API
Your ASP.NET Core API, such as our WeatherApi: typically includes all dependencies and configurations required to run the service. Here’s an example snippet from our Dockerfile:
# Use the .NET SDK image for building the application
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
# Set the working directory inside the container
WORKDIR /src
# Copy the project file to the container
COPY ["WeatherApi.csproj", "./"]
# Restore the dependencies specified in the project file
RUN dotnet restore "WeatherApi.csproj"
# Copy the rest of the application files to the container
COPY . .
# Build the application in Release mode and output the build to /app/build
RUN dotnet build "WeatherApi.csproj" -c Release -o /app/build
# Use the build stage to publish the application
FROM build AS publish
# Publish the application in Release mode to /app/publish
RUN dotnet publish "WeatherApi.csproj" -c Release -o /app/publish
# Use the ASP.NET runtime image for running the application
FROM mcr.microsoft.com/dotnet/aspnet:8.0-jammy AS final
# Set the working directory inside the container
WORKDIR /app
# Copy the published application from the publish stage
COPY --from=publish /app/publish .
# Set the environment variable to configure the application to listen on port 80
ENV ASPNETCORE_URLS=http://+:80
# Expose port 80 to allow external access to the application
EXPOSE 80
# Set the entry point to run the application
ENTRYPOINT ["dotnet", "WeatherApi.dll"]
Some key points in the Dockerfile:
- Multi-stage builds: The Dockerfile uses multiple stages to keep the final image lightweight.
- Restoring dependencies: The
dotnet restore
command ensures all required packages are in place.
- Port configuration: Setting
ASPNETCORE_URLS
and exposing port 80 makes your API available outside the container.
Dockerfile for Blazor App
Likewise, the Blazor app in our WeatherApp project is containerized with a similar Dockerfile. Here’s a simplified version:
# Use the .NET SDK image for building the application
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
# Set the working directory inside the container
WORKDIR /src
# Copy the project file to the container
COPY ["WeatherApp.csproj", "./"]
# Restore the project dependencies
RUN dotnet restore "WeatherApp.csproj"
# Copy the rest of the application files to the container
COPY . .
# Build the application in Release mode and output to /app/build
RUN dotnet build "WeatherApp.csproj" -c Release -o /app/build
# Use the build stage to publish the application
FROM build AS publish
# Publish the application in Release mode to /app/publish
RUN dotnet publish "WeatherApp.csproj" -c Release -o /app/publish
# Use the ASP.NET runtime image for running the application
FROM mcr.microsoft.com/dotnet/aspnet:8.0-jammy AS final
# Set the working directory inside the container
WORKDIR /app
# Copy the published application from the publish stage
COPY --from=publish /app/publish .
# Set the environment variable to configure the application to listen on port 80
ENV ASPNETCORE_URLS=http://+:80
# Expose port 80 for the application
EXPOSE 80
# Define the entry point for the container to run the application
ENTRYPOINT ["dotnet", "WeatherApp.dll"]
Deploying with Docker Compose
While single containers can be very useful, real-world applications often consist of several components that need to work together. Docker Compose simplifies the process of running multi-container applications by using a single YAML file to define and run the entire stack.
Understanding the docker-compose
![Understanding the docker-compose]()
Consider this excerpt from our docker-compose.yml file:
version: '3.8' # Specify the Docker Compose file format version
services:
weather-api: # Service for the Weather API
build: ./WeatherApi # Path to the Dockerfile for building the Weather API
environment:
- ASPNETCORE_ENVIRONMENT=Development # Set the environment to Development
ports:
- "5000:80" # Map port 5000 on the host to port 80 in the container
networks:
- weather-net # Connect this service to the 'weather-net' network
weather-app: # Service for the Weather App (frontend)
build: ./WeatherApp # Path to the Dockerfile for building the Weather App
environment:
- ASPNETCORE_ENVIRONMENT=Development # Set the environment to Development
- ApiUrl=http://192.168.0.104:5000/weatherforecast # URL for the Weather API
ports:
- "8080:80" # Map port 8080 on the host to port 80 in the container
depends_on:
- weather-api # Ensure the Weather API service starts before this service
networks:
- weather-net # Connect this service to the 'weather-net' network
networks:
weather-net: # Define a custom network named 'weather-net'
Key points include
- Service definition: Two services, one for the API and the other for the Blazor app.
- Port mapping: The API maps port 5000 on the host to container port 80, while the Blazor app maps 8080.
- Dependencies: The
depends_on
field ensures that the Weather API is up and running before the Blazor app starts.
- Networking: Both services are on the same Docker network (
weather-net
), enabling seamless communication.
Benefits of Docker Compose
Docker Compose not only streamlines the startup process of multiple containers but also offers advanced features such as scaling, logging, and environment variable management. This makes it a great tool for both local development and staging environments.
Personal Experience with Docker Compose
I recall the first time I used Docker Compose—watching my two containers start-up in unison was exhilarating. It made the deployment process seem less error-prone and more manageable. Soon, I adopted best practices such as versioning my Compose files and using environment variable files (.env) to store configurations.
Best Practices for Dockerizing ASP.NET Core & Blazor Apps
To ensure a smooth transition from development to production, consider these tried-and-true best practices:
- Keep your images lightweight: Use multi-stage builds to ensure the final image only contains what's necessary for runtime.
- Use environment variables: Manage your configurations outside the code. Notice how the Blazor app Dockerfile in our project is used
ENV ASPNETCORE_URLS
for port configuration and how the docker-compose.yml file specifies ApiUrl
.
- Validate the container locally: Before deploying to production, test the containers thoroughly on your local machine. Tools like Docker Desktop on Windows (which I personally use) allow easy monitoring and debugging.
- Monitor container logs: Utilize Docker logs to catch runtime errors or unexpected behaviors. Working on a Windows machine, you can run
docker logs <container_id>
in PowerShell to inspect logs.
- Stay up to date: Docker and .NET continue to evolve fast. Always keep an eye on updates to container images and best practices. This helped me avoid security pitfalls and performance issues in previous projects.
An In-Depth Example: Weather Forecasting App
Let’s look at a practical example combining the ASP.NET Core API and the Blazor front-end. Our application, for instance, comprises a weather forecasting API that provides random temperature data and a Blazor app that displays it.
The API Side
The API project (WeatherApi) features an endpoint /weatherforecast
defined in WeatherForecastController.cs. When you run the container, Swagger UI is available for testing in development environments. Using Docker Compose, the API is accessible on port 5000.
The Front-End Side
The Blazor app (WeatherApp) is designed with interactivity in mind. It calls the weather API via HTTP from components like Weathers.razor, fetching data to display in tables. The API endpoint is configured through an environment variable (ApiUrl=http://192.168.0.104:5000/weatherforecast
), making it straightforward to switch targets if needed.
Running the Complete System
Once both Dockerfiles are ready and your docker-compose file is configured, you create and run the containers with a single command:
docker-compose up --build
![docker-compose up]()
This command builds the containers if they do not already exist, sets up the configured networks, and starts the services. After a few moments, you can navigate to:
http://localhost:5000
to test the API.
![API]()
http://localhost:8080
to interact with the Blazor app.
![Blazor app]()
I remember waiting anxiously the first time I ran this command, watching the images build and then the containers starting up without a hitch! That success gave me the encouragement to experiment more and implement even complex container orchestration scenarios.
In your terminal, run docker images
to list available images and docker ps
to view your running containers, ensuring everything is deployed correctly.
docker ps
![]()
docker images
![]()
Troubleshooting Common Docker Issues
Even with the best practices in place, you may run into some issues when deploying with Docker. Here are some tips:
- Port conflicts: Ensure that the ports you expose on the host are not already in use by another application. For example, you might have to adjust port mappings in docker-compose.yml.
- Missing dependencies: If your build fails due to missing packages, double-check your Dockerfile’s COPY and RUN commands. Sometimes, a minor typo in the project file name can cause the restore step to fail.
- Network issues: Docker Compose handles networks for you, but if the Blazor app cannot reach the API, verify your network configuration and environment variables. Tools like
docker network ls
and docker inspect <container>
are invaluable here.
- Logging and debugging: Utilize
docker logs
to inspect errors. On Windows, you can also use Docker Desktop’s GUI to see container logs and resource usage.
Conclusion
Mastering Docker for deploying ASP.NET Core APIs and Blazor apps can transform your development workflow. The ability to containerize your application ensures consistency, portability, and scalability across environments. In our journey together, we explored building Dockerfiles for our Weather API and Blazor app, configured Docker Compose to orchestrate multi-container deployment, and shared personal insights on overcoming common challenges.
Whether you are a beginner or looking to refine your deployment process, embracing Docker opens a pathway toward smoother, more reliable application releases. As you implement these techniques, remember that every misstep is a learning opportunity—just as it once was for me.
I hope this article has given you a clear, comprehensive roadmap and practical insights into containerizing your ASP.NET Core API and Blazor apps. Happy containerizing, and may your deployments always be as smooth as your code!
If you want to try this deployment yourself, just download the source code as a ZIP, unzip it, and run docker-compose up --build
— your Blazor app and ASP.NET Core API will be up and running in under a minute!