Running Windows Workloads on Kubernetes#
Kubernetes supports Windows worker nodes alongside Linux worker nodes in the same cluster. This enables running Windows-native applications – .NET Framework services, IIS-hosted applications, Windows-specific middleware – on Kubernetes without rewriting them for Linux. However, Windows nodes are not interchangeable with Linux nodes. There are fundamental differences in networking, storage, container runtime behavior, and resource management that you must account for.
Core Constraints#
Before adding Windows nodes, understand what is and is not supported:
Supported:
- Windows Server 2019 and 2022 as worker nodes
- Windows containers (process-isolated and Hyper-V isolated)
- containerd as the container runtime (Docker is deprecated for Kubernetes)
- Most pod spec features: resource requests/limits, liveness/readiness probes, ConfigMaps, Secrets, ServiceAccounts
- Deployments, StatefulSets, DaemonSets, Jobs, CronJobs
Not supported:
- Windows control plane nodes. The control plane (API server, etcd, scheduler, controller-manager) must always run on Linux.
- Privileged containers. Windows does not support the Linux privileged mode.
- Host networking (
hostNetwork: true). Not available on Windows. - Certain volume types: hostPath works but behaves differently. Linux-specific volume types (cephfs, glusterfs) are unsupported.
- Linux capabilities (
securityContext.capabilities). Windows uses a different security model. - Running Linux containers on Windows nodes (and vice versa).
Adding Windows Node Pools#
EKS#
aws eks create-nodegroup \
--cluster-name my-cluster \
--nodegroup-name windows-workers \
--node-role arn:aws:iam::123456789:role/EKSWindowsNodeRole \
--ami-type WINDOWS_CORE_2022_x86_64 \
--instance-types m5.2xlarge \
--scaling-config minSize=1,maxSize=5,desiredSize=2 \
--subnets subnet-abc123 subnet-def456EKS requires the vpc-resource-controller and vpc-admission-webhook addons for Windows pod networking (installed automatically in newer EKS versions).
AKS#
AKS has the strongest native Windows support. It automatically applies kubernetes.io/os=windows labels. Add a taint to prevent Linux pods from scheduling on Windows nodes:
az aks nodepool add \
--resource-group myRG --cluster-name myCluster \
--name winnp --os-type Windows --os-sku Windows2022 \
--node-count 2 --node-vm-size Standard_D4s_v5 \
--node-taints os=windows:NoScheduleGKE#
gcloud container node-pools create win-pool \
--cluster=my-cluster --zone=us-central1-a \
--image-type=WINDOWS_LTSC_CONTAINERD \
--machine-type=n1-standard-4 --num-nodes=2 \
--node-taints=os=windows:NoScheduleScheduling: Ensuring Pods Land on the Right OS#
This is the most critical configuration for hybrid Linux/Windows clusters. Without proper scheduling constraints, Linux pods may attempt to schedule on Windows nodes (and fail) or vice versa.
nodeSelector (Simple Approach)#
Every Kubernetes node is automatically labeled with kubernetes.io/os. Use nodeSelector to target the correct OS:
apiVersion: apps/v1
kind: Deployment
metadata:
name: iis-app
spec:
replicas: 3
selector:
matchLabels:
app: iis-app
template:
metadata:
labels:
app: iis-app
spec:
nodeSelector:
kubernetes.io/os: windows
containers:
- name: iis
image: mcr.microsoft.com/windows/servercore/iis:windowsservercore-ltsc2022
ports:
- containerPort: 80
resources:
requests:
cpu: "500m"
memory: "1Gi"
limits:
cpu: "1"
memory: "2Gi"Every pod in a hybrid cluster should have a nodeSelector for kubernetes.io/os. This includes Linux pods. While Linux pods will naturally fail to run on Windows nodes, they may be scheduled there and enter CrashLoopBackOff, wasting scheduling cycles and potentially blocking Windows capacity.
# Add this to ALL Linux deployments in hybrid clusters
spec:
nodeSelector:
kubernetes.io/os: linuxTaints and Tolerations (Recommended Approach)#
nodeSelector alone is passive – it tells the scheduler where to go but does not prevent other pods from consuming resources on Windows nodes. Use taints on Windows nodes combined with tolerations on Windows workloads:
# Taint Windows nodes (most cloud providers let you do this at node pool creation)
kubectl taint nodes <windows-node> os=windows:NoScheduleapiVersion: apps/v1
kind: Deployment
metadata:
name: dotnet-api
spec:
replicas: 3
template:
spec:
nodeSelector:
kubernetes.io/os: windows
tolerations:
- key: os
operator: Equal
value: windows
effect: NoSchedule
containers:
- name: api
image: myregistry/dotnet-api:6.0
ports:
- containerPort: 5000This dual constraint (nodeSelector plus toleration) is the belt-and-suspenders approach for hybrid clusters. The taint prevents Linux pods from landing on Windows nodes. The nodeSelector directs the Windows pod to the right node type.
RuntimeClass for Container Isolation#
Windows supports two container isolation modes. Process isolation shares the host kernel – fast and lightweight but requires matching Windows versions between container image and host. Hyper-V isolation runs each container in a lightweight VM – provides stronger isolation and allows version mismatches but has higher overhead. Configure Hyper-V isolation via a RuntimeClass with handler runhcs-wcow-hypervisor and reference it with runtimeClassName in the pod spec.
Networking Differences#
Windows networking on Kubernetes works differently from Linux in several important ways.
Service Networking#
Windows supports ClusterIP, NodePort, and LoadBalancer service types. However, the underlying implementation differs:
- kube-proxy on Windows uses the Host Networking Service (HNS) rather than iptables or IPVS. HNS rules can take longer to converge, especially with large numbers of services.
- DNS works the same way (CoreDNS runs on Linux nodes, Windows pods query it normally).
- NetworkPolicy support depends on the CNI plugin. Calico for Windows supports NetworkPolicies. The default Windows CNI (win-bridge or win-overlay) does not.
CNI Plugin Considerations#
| CNI | Windows Support | NetworkPolicy | Notes |
|---|---|---|---|
| Azure CNI | Full | Via Calico | Default for AKS, best Windows experience |
| Calico | Full | Yes | Requires Calico for Windows components |
| Flannel (win-overlay) | Partial | No | Overlay networking, simpler setup |
| Amazon VPC CNI | Via resource controller | Limited | EKS-specific implementation |
No HostPort or HostNetwork#
Windows nodes do not support hostPort or hostNetwork: true in pod specs. If your Linux deployment uses either of these, the Windows equivalent must use a different approach – typically a NodePort or LoadBalancer service.
Storage on Windows Nodes#
Windows nodes support emptyDir (NTFS), hostPath (must use Windows paths like C:\data), ConfigMap/Secret mounts, and PersistentVolumeClaims (via CSI drivers). Azure Disk and Azure File work natively on AKS. EBS works via the EBS CSI driver on EKS. NFS has limited support requiring a Windows NFS client. Important: if your Helm chart hardcodes Linux paths (e.g., /var/data), it will fail on Windows nodes.
Container Image Considerations#
Base Image Matching#
Windows container images must match the host OS version when using process isolation. Running a ltsc2019 container on a ltsc2022 host fails with process isolation (but works with Hyper-V isolation).
| Host OS | Process-Isolated Images | Hyper-V Images |
|---|---|---|
| Windows Server 2022 (LTSC) | ltsc2022 only | ltsc2019, ltsc2022 |
| Windows Server 2019 (LTSC) | ltsc2019 only | ltsc2019 only |
Use multi-arch manifest lists to support multiple Windows versions:
# Dockerfile for Windows (ltsc2022)
FROM mcr.microsoft.com/dotnet/aspnet:8.0-nanoserver-ltsc2022
WORKDIR /app
COPY publish/ .
ENTRYPOINT ["dotnet", "MyApi.dll"]Image Size#
Windows container images are substantially larger than Linux. Nano Server is approximately 280 MB, Server Core is approximately 4.7 GB, and the full Windows image is approximately 9 GB (compared to 5 MB for Alpine Linux). Use nanoserver for .NET applications. Use servercore only when Win32 APIs or IIS are required. Factor large image pull times into readiness probe initial delays and scaling expectations.
Resource Management Differences#
CPU and Memory#
Windows containers support CPU and memory requests and limits, but the enforcement mechanisms differ:
- CPU limits on Windows use Windows Job Objects, which enforce hard CPU caps. There is no CFS-based throttling like Linux.
- Memory limits trigger container termination (similar to Linux OOM kill) when exceeded.
- No swap support in Windows containers.
Resource Recommendations#
Windows nodes have higher baseline resource consumption than Linux. The OS and system services consume approximately 3.5 GB memory and 1.2 CPU, compared to roughly 1.3 GB and 0.6 CPU on Linux. A 4-CPU, 16GB Windows node has roughly 2.8 CPU and 12.5GB available for workloads, compared to 3.4 CPU and 14.7GB on an equivalent Linux node. Size Windows node pools accordingly.
Common Gotchas#
CrashLoopBackOff with no useful logs. Windows container crashes often produce less informative errors than Linux. Check Windows Event Logs inside the container with kubectl exec <pod> -- powershell Get-EventLog -LogName Application -Newest 20.
Service account token mounting. Windows mounts the token at C:\var\run\secrets\kubernetes.io\serviceaccount. Most client libraries handle this automatically, but custom code must use the correct path.
Container restart latency. Windows containers take 10-30 seconds longer to start than Linux. Use startup probes with generous failureThreshold values to handle slow-starting Windows applications.
Windows nodes cannot run standard DaemonSets. Most cluster addons (Prometheus node-exporter, Fluentd, Falco) only work on Linux. Use windows_exporter for Prometheus node metrics and Fluent Bit for logging on Windows nodes.
Group Managed Service Accounts (gMSA). Windows workloads needing Active Directory authentication require gMSA configuration with additional CRDs and webhook admission controllers.