DEV Community

Cover image for Build Once, Deploy Everywhere: Deploying a .NET 8 API with Docker, AKS & GitHub Actions
Ibrahim Bio Abubakar
Ibrahim Bio Abubakar

Posted on

Build Once, Deploy Everywhere: Deploying a .NET 8 API with Docker, AKS & GitHub Actions

Cloud-native development can feel overwhelming at first—containers, Kubernetes, CI/CD pipelines, cloud providers, etc. This tutorial walks you step by step from writing a simple .NET 8 Web API to deploying it on Azure Kubernetes Service (AKS) with a fully automated GitHub Actions CI/CD pipeline.

We’ll go beyond just writing code. You’ll also learn how and why each tool fits into the bigger cloud-native picture.


Learning Objective

By completing this tutorial, you will master:

  • Building production-ready .NET 8 Web APIs

  • Containerizing applications with Docker

  • Deploying to Azure Kubernetes Service (AKS)

  • Setting up CI/CD pipelines with GitHub Actions

  • Applying cloud-native development best practices


Project Overview

We’ll build a Weather Forecast API and deploy it to the cloud.

  • Backend → .NET 8 Web API

  • Containerization → Docker

  • Cloud Platform → Azure Kubernetes Service (AKS)

  • Automation → GitHub Actions for CI/CD


Prerequisites

Required Software

Install these tools (in order):

Visual Studio Code

.NET 8 SDK

Git

Docker Desktop

⚠️ Remember: Start Docker Desktop after installation

Azure CLI

kubectl

GitHub CLI (optional)

Required Accounts

GitHub(free)

Azure (free tier with $200 credits)


Verify Installation

Run the following commands in your terminal:

dotnet --version # Shows installed .NET SDK version (should be 8.x.x) git --version # Confirms Git is installed docker --version # Confirms Docker CLI works az --version # Shows Azure CLI version kubectl version --client # Verifies kubectl client 
Enter fullscreen mode Exit fullscreen mode

Note: If any fail → reinstall that tool.

Image 1


💻 Step 1: Create the .NET Application

1.1 Set Up Project Structure

mkdir weather-app-demo # Create root project folder cd weather-app-demo mkdir WeatherApp # Create app folder cd WeatherApp 
Enter fullscreen mode Exit fullscreen mode

Why? → Organizing code and infra configs in separate folders makes CI/CD pipelines easier to manage.

Image 2


1.2 Initialize .NET Project

dotnet new webapi -minimal 
Enter fullscreen mode Exit fullscreen mode

dotnet new → Creates a new project

webapi → Template for REST APIs

-minimal → Uses the simplified .NET 8 minimal API syntax (less boilerplate)

Image 3


1.3 Add Dependencies

dotnet add package Microsoft.Extensions.Diagnostics.HealthChecks 
Enter fullscreen mode Exit fullscreen mode

HealthChecks → Adds endpoints for monitoring app health (Kubernetes probes rely on this).

Image 4

dotnet add package Swashbuckle.AspNetCore 
Enter fullscreen mode Exit fullscreen mode

Swashbuckle.AspNetCore → Generates Swagger/OpenAPI docs automatically.

Image 5


1.4 Application Code

Replace the content Program.cs with:

var builder = WebApplication.CreateBuilder(args); // Register services builder.Services.AddHealthChecks(); // Adds /health endpoint builder.Services.AddEndpointsApiExplorer(); // Enables endpoint discovery for Swagger builder.Services.AddSwaggerGen(); // Generates Swagger UI // Configure Kestrel to listen on port 8080 (important for containers) builder.WebHost.ConfigureKestrel(options => { options.ListenAnyIP(8080); // Allows traffic from any network interface }); var app = builder.Build(); // Enable Swagger UI in Dev & Prod if (app.Environment.IsDevelopment() || app.Environment.IsProduction()) { app.UseSwagger(); app.UseSwaggerUI(c => { c.SwaggerEndpoint("/swagger/v1/swagger.json", "Weather API v1"); c.RoutePrefix = "swagger"; // URL: /swagger }); } // Welcome endpoint app.MapGet("/", () => new { Message = "Welcome to the Weather App!", Version = "1.0.0", Environment = app.Environment.EnvironmentName, Timestamp = DateTime.UtcNow }) .WithName("GetWelcome") .WithTags("General"); // Weather forecast endpoint app.MapGet("/weather", () => { var summaries = new[] { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" }; var forecast = Enumerable.Range(1, 5).Select(index => new { Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)), // Next 5 days TemperatureC = Random.Shared.Next(-20, 55), // Random °C TemperatureF = 0, // Placeholder Summary = summaries[Random.Shared.Next(summaries.Length)] }) .Select(temp => new { temp.Date, temp.TemperatureC, TemperatureF = 32 + (int)(temp.TemperatureC / 0.5556), // Formula °C → °F temp.Summary }); return forecast; }) .WithName("GetWeatherForecast") .WithTags("Weather"); // Health check endpoint (important for Kubernetes probes) app.MapHealthChecks("/health") .WithTags("Health"); app.Run(); // Starts the web server 
Enter fullscreen mode Exit fullscreen mode

Concepts Explained:

  • Minimal APIs → Introduced in .NET 6, let you build APIs with fewer lines.

  • Swagger → Auto-generates interactive API docs.

  • Probes → Kubernetes uses health endpoints to restart/retry containers when needed.


1.5 Test Locally

dotnet run 
Enter fullscreen mode Exit fullscreen mode

Image 5

Test endpoints in browser or with curl in the terminal:

http://localhost:8080/ → Welcome message

http://localhost:8080/weather → Forecast data

http://localhost:8080/swagger → Swagger docs

http://localhost:8080/health → Health check

Below is the Weather App on a browser:

Image z

Stop the app with Ctrl + C.


🐳 Step 2: Containerize Your Application with Docker

We’ll package the .NET app into a Docker container so it can run consistently anywhere.

2.1 Create Dockerfile

Create a file named Dockerfile inside WeatherApp/ directory:

touch Dockerfile 
Enter fullscreen mode Exit fullscreen mode

Image 6

Add the content below to the Dockerfilecreated:

# Multi-stage build for optimized image size # Runtime stage FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base WORKDIR /app EXPOSE 8080 ENV ASPNETCORE_URLS=http://+:8080 ENV ASPNETCORE_ENVIRONMENT=Production # Build stage FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build ARG BUILD_CONFIGURATION=Release WORKDIR /src # Copy project file and restore dependencies COPY ["WeatherApp/WeatherApp.csproj", "."] RUN dotnet restore "WeatherApp.csproj" # Copy source code and build COPY . . WORKDIR /src COPY WeatherApp/ ./WeatherApp/ WORKDIR /src/WeatherApp RUN dotnet restore "WeatherApp.csproj" RUN dotnet build "WeatherApp.csproj" -c $BUILD_CONFIGURATION -o /app/build # Publish stage FROM build AS publish ARG BUILD_CONFIGURATION=Release RUN dotnet publish "WeatherApp.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false # Final stage FROM base AS final WORKDIR /app COPY --from=publish /app/publish . ENTRYPOINT ["dotnet", "WeatherApp.dll"] 
Enter fullscreen mode Exit fullscreen mode

Concept: Multi-stage builds → build and runtime environments are separate. This keeps the final image small and secure.


2.2 Create .dockerignore

Prevent unnecessary files from bloating your image:

touch .dockerignore 
Enter fullscreen mode Exit fullscreen mode

Image 7

Add the content below to the Dockerfilecreated:

 # Build outputs bin/ obj/ out/ # IDE files .vs/ .vscode/ *.user *.suo # OS files .DS_Store Thumbs.db # Git .git/ .gitignore # Docs README.md *.md # Docker Dockerfile* .dockerignore # Logs *.log logs/ 
Enter fullscreen mode Exit fullscreen mode

2.3 Build & Test Container

# Build image docker build -t weather-app:local . # Ensure your Docker Desktop is running before you run this command 
Enter fullscreen mode Exit fullscreen mode

Image 8

Below is the built WeatherApp image in Docker Desktop app:

Image 9

Now let's run the container:

# Run container docker run -d -p 8080:8080 --name weather-test weather-app:local 
Enter fullscreen mode Exit fullscreen mode

Image 10

Test the running app using curl in the terminal:

# Test endpoints curl http://localhost:8080/ curl http://localhost:8080/weather 
Enter fullscreen mode Exit fullscreen mode

Image 11

# Clean up docker stop weather-test docker rm weather-test 
Enter fullscreen mode Exit fullscreen mode

Image 12

Concept: Containers make your app portable — works the same on your laptop and in the cloud.


Step 3: Set Up Azure Infrastructure

We’ll use Azure CLI to provision resources.

3.1 Login to Azure

az login #Opens browser for Azure sign-in. 
Enter fullscreen mode Exit fullscreen mode

3.2 Select Subscription

az account list --output table # See subscriptions az account set --subscription "Your-Subscription-Name" 
Enter fullscreen mode Exit fullscreen mode

3.3 Create Resource Group

az group create --name student-demo --location eastus 
Enter fullscreen mode Exit fullscreen mode

Image 14

Concept: A resource group is a logical container for all your Azure resources.


3.4 Create Container Registry (ACR)

az acr create --resource-group student-demo --name studentdemo2042acr --sku Basic 
Enter fullscreen mode Exit fullscreen mode

Image 15

Concept: ACR = private Docker Hub inside Azure.


3.5 Build & Push Image

az acr build --registry studentdemo2042acr --image weather-app:latest . 
Enter fullscreen mode Exit fullscreen mode

Image 16

Why ACR build? → Azure builds inside the cloud, avoiding OS/CPU compatibility issues.


3.6 Create AKS Cluster

az aks create \ --resource-group student-demo \ --name student-aks-cluster \ --node-count 1 \ --node-vm-size Standard_B2s \ --attach-acr studentdemo2042acr \ --enable-managed-identity \ --generate-ssh-keys 
Enter fullscreen mode Exit fullscreen mode

Image 17

📌 Concept:

  • AKS = Azure-managed Kubernetes cluster

  • attach-acr → avoids ImagePullBackOff errors by linking AKS to ACR automatically


3.7 Connect to Cluster

az aks get-credentials \ --resource-group student-demo \ --name student-aks-cluster 
Enter fullscreen mode Exit fullscreen mode

Image 18

kubectl get nodes # Verify cluster connection 
Enter fullscreen mode Exit fullscreen mode

Image 19


☸️ Step 4: Deploy with Kubernetes

Kubernetes uses YAML manifests to declare your app’s state.

cd .. # Back to weather-app-demo folder mkdir k8s cd k8s touch deployment.yaml 
Enter fullscreen mode Exit fullscreen mode

Image 20

4.1 Create Deployment

Copy and paste the content below in k8s/deployment.yaml

 apiVersion: apps/v1 kind: Deployment metadata: name: weather-app labels: app: weather-app version: v1 spec: replicas: 2 # Run 2 pods for high availability selector: matchLabels: app: weather-app template: metadata: labels: app: weather-app version: v1 spec: containers: - name: weather-app image: studentdemo2024acr.azurecr.io/weather-app:latest imagePullPolicy: Always ports: - containerPort: 8080 env: - name: ASPNETCORE_ENVIRONMENT value: "Production" resources: # Resource requests & limits requests: memory: "128Mi" cpu: "100m" limits: memory: "512Mi" cpu: "500m" # Probes for self-healing livenessProbe: httpGet: path: /health port: 8080 initialDelaySeconds: 30 readinessProbe: httpGet: path: /health port: 8080 initialDelaySeconds: 10 
Enter fullscreen mode Exit fullscreen mode

📌 Concepts:

  • Deployment → ensures the desired # of pods run.

  • Probes → tell Kubernetes when a pod is healthy or ready.


4.2 Create Service

Create k8s/service.yaml file

touch service.yaml 
Enter fullscreen mode Exit fullscreen mode

Image 22

Copy and paste the content below in k8s/service.yaml

apiVersion: v1 kind: Service metadata: name: weather-app-service spec: type: LoadBalancer # Exposes app to internet selector: app: weather-app ports: - port: 80 # External port targetPort: 8080 # Maps to container 
Enter fullscreen mode Exit fullscreen mode

📌 Concept: A Service gives your pods a stable IP address + load balancing.


4.3 Deploy

# Apply Kubernetes manifests kubectl apply -f deployment.yaml kubectl apply -f service.yaml 
Enter fullscreen mode Exit fullscreen mode

Image 23

# Check deployment status kubectl get deployments kubectl get pods 
Enter fullscreen mode Exit fullscreen mode

Image 24

# Check deployment status kubectl get services 
Enter fullscreen mode Exit fullscreen mode

Image 25

🔄 Step 5: GitHub Actions CI/CD

We’ll automate build → push → deploy.

5.1 Init Git

cd .. # Back to weather-app-demo folder # Initialize Git repository git init git add . git commit -m "Initial commit: Weather App" 
Enter fullscreen mode Exit fullscreen mode

Image 26

5.2 Create Service Principal

# Get your subscription ID SUBSCRIPTION_ID=$(az account show --query id --output tsv) echo "Subscription ID: $SUBSCRIPTION_ID" # Create service principal with contributor role az ad sp create-for-rbac \ --name "weather-app-github-sp" \ --role contributor \ --scopes /subscriptions/$SUBSCRIPTION_ID/resourceGroups/student-demo \ --sdk-auth 
Enter fullscreen mode Exit fullscreen mode

Image 27

Save JSON output → add as GitHub Secret AZURE_CREDENTIALS.


5.3 Create GitHub Repo

gh auth login # Follow the prompts 
Enter fullscreen mode Exit fullscreen mode

Image 28

Image 29

# Create GitHub repository (using GitHub CLI) and Push to Repo gh repo create weather-app-demo --public --source=. --push 
Enter fullscreen mode Exit fullscreen mode

Alternative: Create the repository manually on GitHub.com and push your code.

Image 30

Image 31

5.4 Configure GitHub Secrets

Use JSON output from step 5.2

gh secret set AZURE_CREDENTIALS -b'PASTE_JSON_HERE' 
Enter fullscreen mode Exit fullscreen mode

Image 32

gh secret set ACR_NAME -b"studentdemo2042acr" 
Enter fullscreen mode Exit fullscreen mode

Image 33

gh secret set RESOURCE_GROUP -b"student-demo" 
Enter fullscreen mode Exit fullscreen mode

Image 34

gh secret set CLUSTER_NAME -b"student-aks-cluster" 
Enter fullscreen mode Exit fullscreen mode

Image 35

5.5 Create a GitHub Workflow

Create the workflow directory and file:

 mkdir -p .github/workflows touch .github/workflows/deploy.yml 
Enter fullscreen mode Exit fullscreen mode

Copy and paste the content below in .github/workflows/deploy.yml file.

name: Build and Deploy to AKS on: push: branches: [ main ] pull_request: branches: [ main ] env: IMAGE_NAME: weather-app jobs: build-and-deploy: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 - name: Setup .NET uses: actions/setup-dotnet@v3 with: dotnet-version: '9.0.x' - name: Restore dependencies run: dotnet restore WeatherApp/WeatherAPP.csproj - name: Build application run: dotnet build WeatherApp/WeatherAPP.csproj --configuration Release --no-restore - name: Run tests run: dotnet test WeatherApp/WeatherAPP.csproj --no-build --verbosity normal || true - name: Azure Login uses: azure/login@v1 with: creds: ${{ secrets.AZURE_CREDENTIALS }} - name: Setup Azure CLI uses: azure/cli@v2 with: inlineScript: echo "Azure CLI setup complete" - name: Build and push Docker image to ACR run: | IMAGE_TAG=${{ secrets.ACR_NAME }}.azurecr.io/${{ env.IMAGE_NAME }}:${{ github.sha }} az acr build \ --registry ${{ secrets.ACR_NAME }} \ --image ${{ env.IMAGE_NAME }}:${{ github.sha }} \ --file WeatherApp/Dockerfile \ . echo "IMAGE_TAG=$IMAGE_TAG" >> $GITHUB_ENV - name: Deploy to AKS if: github.ref == 'refs/heads/main' run: | echo "Checking if AKS cluster exists..." if ! az aks show --resource-group "${{ secrets.RESOURCE_GROUP }}" --name "${{ secrets.CLUSTER_NAME }}" --output table; then echo "ERROR: AKS cluster '${{ secrets.CLUSTER_NAME }}' not found in resource group '${{ secrets.RESOURCE_GROUP }}'" exit 1 fi echo "Getting AKS credentials..." az aks get-credentials \ --resource-group ${{ secrets.RESOURCE_GROUP }} \ --name ${{ secrets.CLUSTER_NAME }} \ --overwrite-existing echo "Testing kubectl connection..." kubectl cluster-info echo "Updating deployment with image: ${{ env.IMAGE_TAG }}" kubectl set image deployment/weather-app \ weather-app=${{ env.IMAGE_TAG }} echo "Waiting for rollout to complete..." kubectl rollout status deployment/weather-app --timeout=600s echo "Deployment status:" kubectl get pods -l app=weather-app kubectl get service weather-app-service 
Enter fullscreen mode Exit fullscreen mode

📌 Concept:

  • GitHub Actions → automates the whole pipeline.

  • Every push to main → build → push image → deploy to AKS.

5.6 Deploy Your Application

git add . git commit -m "Add GitHub Actions CI/CD pipeline" git push origin main 
Enter fullscreen mode Exit fullscreen mode

Image 35

Monitor the deployment:

  • Go to your GitHub repository
  • Click the Actions tab
  • Watch your workflow run in real-time

Image 36


🌐 Step 6: Access Your Deployed App

6.1 Get External IP Address

# Check service status kubectl get service weather-app-service 
Enter fullscreen mode Exit fullscreen mode

Image 38

# Wait for EXTERNAL-IP (may take 2-5 minutes) kubectl get service weather-app-service --watch 
Enter fullscreen mode Exit fullscreen mode

Wait until EXTERNAL-IP is assigned. Then test:

# Test the endpoints curl http://YOUR-EXTERNAL-IP/ curl http://YOUR-EXTERNAL-IP/weather curl http://YOUR-EXTERNAL-IP/health 
Enter fullscreen mode Exit fullscreen mode

Image 40////

# Or open in browser open http://YOUR-EXTERNAL-IP/swagger # macOS start http://YOUR-EXTERNAL-IP/swagger # Windows 
Enter fullscreen mode Exit fullscreen mode

Image 41


🔄 Step 7: Test Continuous Deployment

  1. Edit Program.cs welcome message.
app.MapGet("/", () => new { Message = "Welcome to the Updated Weather App! 🌤️", Version = "1.1.0", Environment = app.Environment.EnvironmentName, Timestamp = DateTime.UtcNow, DeployedBy = "GitHub Actions" }) 
Enter fullscreen mode Exit fullscreen mode
  1. Commit & push:
git add . git commit -m "Update welcome message" git push origin main 
Enter fullscreen mode Exit fullscreen mode
  1. GitHub Actions will rebuild & redeploy automatically.

Image 42

  1. Test again → you should see your new message.
curl http://YOUR-EXTERNAL-IP/ 
Enter fullscreen mode Exit fullscreen mode

Image 43


Conclusion

You’ve just taken a local .NET 8 Web API and transformed it into a cloud-native application running on Azure Kubernetes Service, packaged with Docker, and automated with a GitHub Actions CI/CD pipeline. That’s the full lifecycle of modern cloud development — from writing code → to shipping containers → to running resilient workloads in production.

The best part? You now have a repeatable blueprint you can apply to almost any project:

Swap the Weather API with your own service

Reuse the Docker + Kubernetes setup

Extend your GitHub workflow for tests, security scans, or staging environments

This isn’t just about building an app — it’s about learning how to ship production-ready software at scale.

Keep experimenting: add monitoring, autoscaling, or connect a database. The cloud-native journey is iterative, but now you’ve got the foundations locked in.

Top comments (0)