How to Deploy Containerised Applications to Kubernetes on Windows Server 2025

Kubernetes supports Windows worker nodes, making it possible to run Windows containers — including ASP.NET Framework and ASP.NET Core applications — alongside Linux workloads in the same cluster. Windows Server 2025 is compatible with Kubernetes 1.29 and later, and Microsoft’s AKS (Azure Kubernetes Service) natively supports Windows node pools. If you are running a self-managed cluster on-premises, you can join Windows Server 2025 nodes to an existing Linux control plane cluster. This tutorial walks through deploying a containerised ASP.NET application to a Kubernetes cluster with a Windows worker node, covering the Windows-specific YAML configurations, storage, networking limitations, and monitoring considerations you need to know.

Prerequisites

  • A Kubernetes cluster with at least one Linux control plane node (Kubernetes 1.29+).
  • One or more Windows Server 2025 worker nodes joined to the cluster (using Calico, Flannel, or Antrea for CNI — all support Windows).
  • Alternatively, an AKS cluster with a Windows node pool created.
  • kubectl configured with access to the cluster.
  • A private or public container registry with your Windows container image pushed.
  • Docker or containerd installed on the Windows worker nodes (containerd is the default container runtime for Kubernetes 1.24+).
  • Familiarity with basic Kubernetes concepts: Pods, Deployments, Services, and Ingress.

Step 1: Join a Windows Server 2025 Node to the Cluster

For self-managed clusters, join the Windows node using the kubeadm token. Run the following on the Windows Server 2025 node after installing containerd and the Kubernetes node binaries.

# On Windows Server 2025 worker node

# Install containerd (via winget or manual download)
winget install --id=containerd.containerd -e

# Enable required features
Enable-WindowsOptionalFeature -Online -FeatureName containers -All -NoRestart
Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Hyper-V -All -NoRestart
Restart-Computer -Force

# After reboot — configure kubelet and join the cluster
# (Token and hash are generated on the Linux control plane with: kubeadm token create --print-join-command)
kubeadm join 10.0.0.10:6443 `
    --token abcdef.0123456789abcdef `
    --discovery-token-ca-cert-hash sha256:<hash> `
    --node-labels "kubernetes.io/os=windows"
# On the Linux control plane — verify the Windows node joined
kubectl get nodes -o wide
# Windows node should appear with OS=Windows

Step 2: Understand Windows Container Limitations in Kubernetes

Before deploying, be aware of the key constraints that differentiate Windows pods from Linux pods:

  • No privileged containers — Windows does not support privileged mode. Avoid securityContext.privileged: true.
  • No DaemonSet HostNetwork — Windows pods cannot use hostNetwork: true in DaemonSets. Use a host-networking alternative or configure CNI accordingly.
  • No Linux-specific syscalls — eBPF, inotify, and similar Linux kernel APIs are unavailable.
  • Image version matching — Windows container images must match the host OS build version (e.g., a Windows Server 2025 host requires images built with FROM mcr.microsoft.com/windows/servercore:ltsc2025).
  • RunAsUser — Windows does not use numeric UIDs; use runAsUserName instead.

Step 3: Create a Windows Container Deployment YAML

Use the nodeSelector field to schedule the Pod on a Windows node. Without this selector, the Kubernetes scheduler may attempt to place the Windows image on a Linux node where it will fail to start.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: aspnet-app
  namespace: default
spec:
  replicas: 2
  selector:
    matchLabels:
      app: aspnet-app
  template:
    metadata:
      labels:
        app: aspnet-app
    spec:
      nodeSelector:
        kubernetes.io/os: windows
      containers:
        - name: aspnet-app
          image: registry.corp.local/myproject/aspnet-app:ltsc2025
          ports:
            - containerPort: 80
          resources:
            requests:
              cpu: "500m"
              memory: "512Mi"
            limits:
              cpu: "2"
              memory: "2Gi"
          env:
            - name: ASPNETCORE_ENVIRONMENT
              value: Production
      tolerations:
        - key: "os"
          operator: "Equal"
          value: "windows"
          effect: "NoSchedule"
# Apply the deployment
kubectl apply -f aspnet-deployment.yaml

# Check pod status
kubectl get pods -o wide

# View events if pods are not starting
kubectl describe pod -l app=aspnet-app

Step 4: Expose the Application with a Service and Ingress

Create a ClusterIP Service to expose the Deployment internally, then use an Ingress resource to route external HTTP traffic. The NGINX Ingress Controller works with Windows nodes when deployed on Linux nodes and proxying to Windows pod Services.

apiVersion: v1
kind: Service
metadata:
  name: aspnet-app-svc
  namespace: default
spec:
  selector:
    app: aspnet-app
  ports:
    - protocol: TCP
      port: 80
      targetPort: 80
  type: ClusterIP
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: aspnet-app-ingress
  namespace: default
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
spec:
  ingressClassName: nginx
  rules:
    - host: aspnet.corp.local
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: aspnet-app-svc
                port:
                  number: 80
kubectl apply -f aspnet-service-ingress.yaml
kubectl get ingress

Step 5: Configure Persistent Storage for Windows Pods

Windows pods support persistent volumes backed by SMB shares (using the SMB CSI driver) or Azure Disk/Azure Files in AKS. Local hostPath volumes work for single-node testing but are not suitable for production.

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: aspnet-app-pvc
  namespace: default
spec:
  accessModes:
    - ReadWriteOnce
  storageClassName: smb-csi
  resources:
    requests:
      storage: 10Gi
# Install the SMB CSI driver for Windows node support
helm repo add csi-driver-smb https://raw.githubusercontent.com/kubernetes-csi/csi-driver-smb/master/charts
helm install csi-driver-smb csi-driver-smb/csi-driver-smb `
    --namespace kube-system `
    --set windows.enabled=true

Step 6: Monitor Windows Nodes with windows_exporter and Grafana

The windows_exporter Prometheus exporter exposes Windows performance metrics, including Hyper-V and container stats. Deploy it as a DaemonSet targeting Windows nodes.

# On each Windows node — install windows_exporter
$ExporterUrl = "https://github.com/prometheus-community/windows_exporter/releases/download/v0.27.0/windows_exporter-0.27.0-amd64.exe"
Invoke-WebRequest -Uri $ExporterUrl -OutFile "C:windows_exporterwindows_exporter.exe"

# Register as a Windows service with container collector enabled
New-Service -Name "windows_exporter" `
    -BinaryPathName "C:windows_exporterwindows_exporter.exe --collectors.enabled=cpu,cs,container,hyperv,memory,net,os,process" `
    -StartupType Automatic
Start-Service -Name "windows_exporter"
# Prometheus scrape config addition (prometheus.yml)
# - job_name: 'windows-nodes'
#   static_configs:
#     - targets: ['win-node01:9182', 'win-node02:9182']

Import the Windows Exporter Dashboard (Grafana dashboard ID 14694) into Grafana to visualise CPU, memory, network, and container metrics from your Windows nodes alongside Linux node dashboards.

Conclusion

Kubernetes on Windows Server 2025 enables organisations to modernise Windows workloads — particularly ASP.NET applications — without abandoning Kubernetes as the orchestration platform. The key discipline is always specifying nodeSelector: kubernetes.io/os: windows on Windows Deployments, matching the container image OS version to the host, and using Windows-compatible CSI drivers for persistent storage. Monitoring with windows_exporter and Grafana provides the same observability for Windows nodes that you have for Linux nodes, giving you a unified view of your mixed-OS cluster.