The final step of mastery is getting your API out of your localhost and onto the internet. Dockerizing your ASP.NET Core API ensures that it will run flawlessly on any operating system, eliminating the notorious "It works on my machine!" problem.
ASP.NET Core uses a Two-Stage Docker build. The first stage contains the heavy .NET SDK to compile the C# code. The second stage contains only the incredibly lightweight Runtime, discarding the heavy compilation tools to create a tiny production image.
# STAGE 1: BUILD ENVIRONMENT
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
# Copy only the project file first to cache NuGet restores
COPY ["MyApp.Api/MyApp.Api.csproj", "MyApp.Api/"]
RUN dotnet restore "MyApp.Api/MyApp.Api.csproj"
# Copy the rest of the code and physically compile it
COPY . .
WORKDIR "/src/MyApp.Api"
RUN dotnet publish "MyApp.Api.csproj" -c Release -o /app/publish
# STAGE 2: PRODUCTION RUNTIME ENVIRONMENT
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS final
WORKDIR /app
# Listen on port 8080 (the new default for .NET 8 Docker images)
EXPOSE 8080
# Copy the compiled DLLs from Stage 1 into Stage 2
COPY --from=build /app/publish .
# The command that executes when the container boots up
ENTRYPOINT ["dotnet", "MyApp.Api.dll"]
Using GitHub Actions, we can tell GitHub to automatically compile our C# code, run our xUnit tests, build the Docker Image, and push it to Azure App Service every time we push code to the main branch.
name: Build, Test, and Deploy to Azure
on:
push:
branches: [ "main" ]
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up .NET Core
uses: actions/setup-dotnet@v3
with:
dotnet-version: '8.0.x'
- name: Run xUnit Tests
# The CI/CD pipeline instantly halts here if a test fails!
run: dotnet test ./MyApp.Tests/MyApp.Tests.csproj
- name: Build and Push Docker Image
uses: docker/build-push-action@v4
with:
context: .
push: true
tags: myazurecr.azurecr.io/myapp-api:latest
- name: Deploy to Azure Web App
uses: azure/webapps-deploy@v2
with:
app-name: 'my-production-api'
images: 'myazurecr.azurecr.io/myapp-api:latest'
When running in Docker/Azure, your appsettings.json is baked into the image. You must NEVER hardcode the Production SQL Password or the JWT Secret Key into that JSON file. Instead, you supply them via OS Environment Variables.
# The Docker run command injects the secret dynamically
docker run -d -p 8080:8080 -e "ConnectionStrings__DefaultConnection=Server=sql.prod;Password=SuperSecret!" myazurecr.azurecr.io/myapp-api:latest
Q: "Why do we use two completely different base images (`mcr.microsoft.com/dotnet/sdk:8.0` and `mcr.microsoft.com/dotnet/aspnet:8.0`) inside the exact same Dockerfile?"
Architect Answer: "Security and performance. The `SDK` image is over 800MB. It contains the C# compiler, NuGet package manager, MSBuild tools, and debugger—everything needed to turn source code into an executable. If we deploy the SDK to production, not only is our Docker container massive, but if a hacker breaches our container, they now have a full suite of compiling tools to build malware directly on our server. By utilizing a Multi-Stage Dockerfile, we do the heavy compiling inside the SDK image during 'Stage 1', copy ONLY the final `.dll` binaries into 'Stage 2', and then completely throw away Stage 1. Stage 2 uses the `aspnet` Runtime image, which is less than 200MB, contains zero development tools, pulls instantly, and significantly reduces the attack surface."
You have achieved total mastery of ASP.NET Core Web API. From basic REST Principles to JWT Security, Dockerization, and asynchronous architecture, you are now equipped to build enterprise-grade microservices.