Skip to main content
Launch week·Five new features shipping this week (March 30 – April 3)

Windmill on Azure

Windmill can be deployed on Azure using AKS (Azure Kubernetes Service) with an Azure Database for PostgreSQL Flexible Server. This guide walks through the full setup: networking, database, Helm deployment, and ingress. It then covers optional Entra ID passwordless authentication for Enterprise users.

Adapt replica counts and VM sizes to your workload. As a rule of thumb, allocate 1 worker per vCPU and 1–2 GB of RAM per worker.

Prerequisites

Create a resource group

az group create --name <rg> --location <region>

Create an AKS cluster

az aks create \
--name <aks-cluster> \
--resource-group <rg> \
--node-count 3 \
--node-vm-size Standard_D4s_v3 \
--network-plugin azure \
--generate-ssh-keys

Get credentials to interact with the cluster:

az aks get-credentials --name <aks-cluster> --resource-group <rg>
tip

If you plan to use Entra ID database authentication, enable the OIDC issuer and Workload Identity add-on now to avoid a cluster restart later:

az aks update \
--name <aks-cluster> \
--resource-group <rg> \
--enable-oidc-issuer \
--enable-workload-identity

Create a PostgreSQL Flexible Server

Server creation

az postgres flexible-server create \
--name <pg-server> \
--resource-group <rg> \
--tier GeneralPurpose \
--sku-name Standard_D4s_v3 \
--storage-size 256 \
--admin-user windmill \
--admin-password '<strong-password>' \
--version 16

Sizing guidance

  • Instance size: Windmill requires roughly 50 connections per server plus 5 per worker. Standard_D4s_v3 supports up to 859 connections by default, which gives room to grow.
  • Storage: Windmill stores logs and job results in the database. A good starting point is 256 GiB with autoscaling enabled. Adjust based on your job retention policy.
  • High availability: For production, enable zone-redundant HA with --high-availability ZoneRedundant.
  • Backups: Automated backups are enabled by default (7-day retention). Consider increasing retention for production.

Create the windmill database

az postgres flexible-server db create \
--server-name <pg-server> \
--resource-group <rg> \
--database-name windmill

Network access

If the AKS cluster and PostgreSQL server are in the same VNet, use private access (VNet integration). Otherwise, use private endpoints or allow access from the AKS outbound IPs via firewall rules.

To add a firewall rule for a specific IP:

az postgres flexible-server firewall-rule create \
--name <pg-server> \
--resource-group <rg> \
--rule-name allow-aks \
--start-ip-address <aks-outbound-ip> \
--end-ip-address <aks-outbound-ip>

Database role setup

Azure Flexible Server does not provide a true PostgreSQL superuser. The standard init-db-as-superuser.sql uses WITH BYPASSRLS which is not available on Azure. Connect to the windmill database as the admin user and run the following adapted setup instead:

-- Create the windmill roles without BYPASSRLS
CREATE ROLE windmill_user;
CREATE ROLE windmill_admin;
GRANT windmill_user TO windmill_admin;

-- Grant schema access
GRANT USAGE ON SCHEMA public TO windmill_admin;
GRANT USAGE ON SCHEMA public TO windmill_user;

-- Grant the roles to your login user (the user in DATABASE_URL)
-- For password auth: GRANT windmill_admin TO windmill;
-- For Entra ID: GRANT windmill_admin TO "windmill-identity";
GRANT windmill_admin TO <your-login-user>;
GRANT windmill_user TO <your-login-user>;
note

On recent Windmill versions (>= 1.414.0), Windmill automatically creates admin policies on all RLS-enabled tables as a fallback when BYPASSRLS is not available. No manual policy creation is needed.

See running Windmill without a Postgres superuser for more details.

Deploy Windmill with Helm

helm repo add windmill https://windmill-labs.github.io/windmill-helm-charts/
helm repo update

Create a values.yaml:

windmill:
baseDomain: "windmill.example.com"
baseProtocol: "https"
databaseUrl: postgresql://windmill:<strong-password>@<pg-server>.postgres.database.azure.com:5432/windmill?sslmode=require

postgresql:
enabled: false

Install:

helm install windmill windmill/windmill \
--namespace windmill \
--create-namespace \
-f values.yaml

For Enterprise Edition, set enterprise.enabled: true in your values.yaml. The chart automatically uses the windmill-ee image when enterprise is enabled. See Helm chart documentation for all available options.

Expose Windmill

You can expose Windmill using an Azure Load Balancer (L4) or the Application Gateway Ingress Controller (AGIC, L7). For HTTPS, we recommend terminating TLS at the ingress layer.

Using Application Gateway (AGIC)

AGIC is not installed by default. Enable the add-on on your AKS cluster:

az aks enable-addons \
--name <aks-cluster> \
--resource-group <rg> \
--addons ingress-appgw \
--appgw-name windmill-appgw \
--appgw-subnet-cidr "<available-/24-in-your-vnet>"

This creates an Application Gateway instance automatically. See the AGIC add-on tutorial for detailed setup options.

The Windmill Helm chart already includes an Ingress resource with the correct path routing (/ws/, /ws_mp/, /). Configure it for AGIC in your values.yaml:

ingress:
enabled: true
className: "azure-application-gateway"
annotations:
appgw.ingress.kubernetes.io/ssl-redirect: "true"
appgw.ingress.kubernetes.io/request-timeout: "3600"
tls:
- hosts:
- windmill.example.com
secretName: windmill-tls

The request-timeout of 3600 seconds prevents Application Gateway from closing WebSocket connections (LSP and multiplayer) after the default 30-second timeout.

Point your domain to the Application Gateway's public IP and configure a TLS certificate (e.g. via cert-manager, Azure Key Vault, or a manual TLS secret). Then set the Base URL in Windmill instance settings.

Open Windmill

Navigate to your domain or load balancer IP. You should see the Windmill login screen. The default credentials are admin@windmill.dev / changeme. Follow the setup wizard to configure your instance.

Entra ID database authentication (optional)

Windmill Enterprise supports Azure Workload Identity for passwordless authentication to PostgreSQL Flexible Server via Microsoft Entra ID. Instead of a static password, Windmill obtains short-lived Azure AD tokens that are automatically refreshed in the background.

Enterprise feature

Azure Entra ID database authentication is only available in Windmill Enterprise Edition.

Enable Workload Identity on AKS

If you did not enable it during cluster creation:

az aks update \
--name <aks-cluster> \
--resource-group <rg> \
--enable-oidc-issuer \
--enable-workload-identity

Create a managed identity with federated credential

# Create identity
az identity create \
--name windmill-identity \
--resource-group <rg>

# Get the identity client ID and AKS OIDC issuer URL
export IDENTITY_CLIENT_ID=$(az identity show --name windmill-identity --resource-group <rg> --query clientId -o tsv)
export AKS_OIDC_ISSUER=$(az aks show --name <aks-cluster> --resource-group <rg> --query "oidcIssuerProfile.issuerUrl" -o tsv)

# Create the federated credential
az identity federated-credential create \
--name windmill-federated \
--identity-name windmill-identity \
--resource-group <rg> \
--issuer "$AKS_OIDC_ISSUER" \
--subject system:serviceaccount:windmill:windmill \
--audiences api://AzureADTokenExchange

The --subject format is system:serviceaccount:<namespace>:<service-account-name>. The service account name is generated by the Helm chart's fullname template (matching the release name). Replace both values if you use a different namespace or release name.

Enable Entra auth on PostgreSQL

az postgres flexible-server update \
--name <pg-server> \
--resource-group <rg> \
--microsoft-entra-auth Enabled

Set the Entra admin

Assign the managed identity as the Entra administrator of the Flexible Server.

az postgres flexible-server microsoft-entra-admin create \
--server-name <pg-server> \
--resource-group <rg> \
--display-name windmill-identity \
--object-id $(az identity show --name windmill-identity --resource-group <rg> --query principalId -o tsv) \
--type ServicePrincipal

Create the Entra-authenticated database role

Connect using an Azure AD token and create the principal:

export PGPASSWORD=$(az account get-access-token --resource-type oss-rdbms --query accessToken -o tsv)
psql "host=<pg-server>.postgres.database.azure.com dbname=postgres user=windmill-identity sslmode=require"
-- pgaadauth_create_principal must be run from the postgres database
SELECT * FROM pgaadauth_create_principal('windmill-identity', false, false);

Then grant database access and set up the Windmill roles as described in the database role setup section above, using "windmill-identity" as the login user:

\c windmill
GRANT ALL ON DATABASE windmill TO "windmill-identity";
note

pgaadauth_create_principal only exists in the postgres database. Always connect to postgres before calling it.

Configure Windmill for Entra ID

Set the DATABASE_URL environment variable with entraid as the password sentinel:

postgresql://windmill-identity:entraid@<pg-server>.postgres.database.azure.com:5432/windmill?sslmode=require

When Windmill detects entraid as the password, it switches to Azure AD token-based authentication.

When Workload Identity is configured on AKS, these environment variables are automatically injected into pods by the mutating webhook — you do not need to set them manually:

VariableDescription
AZURE_TENANT_IDYour Azure AD tenant ID
AZURE_CLIENT_IDClient ID of the managed identity
AZURE_FEDERATED_TOKEN_FILEPath to the projected service account token
AZURE_AUTHORITY_HOSTAzure AD authority endpoint

For sovereign clouds (Azure Government, Azure China), the webhook sets AZURE_AUTHORITY_HOST to the appropriate endpoint automatically.

Helm values for Entra ID

windmill:
baseDomain: "windmill.example.com"
baseProtocol: "https"
databaseUrl: postgresql://windmill-identity:entraid@<pg-server>.postgres.database.azure.com:5432/windmill?sslmode=require

app:
labels:
azure.workload.identity/use: "true"

indexer:
labels:
azure.workload.identity/use: "true"

workerGroups:
- name: default
replicas: 2
labels:
azure.workload.identity/use: "true"
- name: native
replicas: 1
labels:
azure.workload.identity/use: "true"

serviceAccount:
annotations:
azure.workload.identity/client-id: "<IDENTITY_CLIENT_ID>"

enterprise:
enabled: true

postgresql:
enabled: false

The azure.workload.identity/use: "true" label must be present on every pod that connects to the database (app, indexer, and all worker groups).

How it works

  1. AKS projects a short-lived service account token into each pod via the Workload Identity webhook.
  2. At startup, Windmill detects the entraid sentinel password and reads the projected token from AZURE_FEDERATED_TOKEN_FILE.
  3. Windmill exchanges the federated token for an Azure AD access token scoped to https://ossrdbms-aad.database.windows.net/.default.
  4. The access token is used as the PostgreSQL password when opening connections.
  5. A background task refreshes the token approximately every 24 hours, before the previous token expires. Active connections are re-authenticated transparently.