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.
kubectlconfigured 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: truein 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
runAsUserNameinstead.
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.