|
| 1 | +--- |
| 2 | +title: "Host a Durable Functions app in Azure Container Apps" |
| 3 | +description: Learn how to host a Durable Functions app using the MSSQL backend in Azure Container Apps. |
| 4 | +ms.topic: how-to |
| 5 | +ms.date: 05/06/2025 |
| 6 | +--- |
| 7 | + |
| 8 | +# Host a Durable Functions app in Azure Container Apps (.NET isolated) |
| 9 | + |
| 10 | +This article shows how to host a Durable Functions app in Azure Container Apps. While Durable Functions supports several [storage providers](./durable-functions-storage-providers.md) or *backends*, autoscaling of the app is only available when using the Microsoft SQL (MSSQL) backend. If another backends is used, you need to [manually set up scaling](../functions-container-apps-hosting.md#event-driven-scaling) today. |
| 11 | + |
| 12 | +## Prerequisites |
| 13 | + |
| 14 | ++ [Visual Studio Code](https://code.visualstudio.com/download) installed. |
| 15 | + |
| 16 | ++ [.NET 8.0 SDK](https://dotnet.microsoft.com/download). |
| 17 | + |
| 18 | ++ [Docker](https://docs.docker.com/install/) and [Docker ID](https://hub.docker.com/signup) |
| 19 | + |
| 20 | ++ [Azure CLI](/cli/azure/install-azure-cli) [version 2.47](/cli/azure/release-notes-azure-cli#april-21-2020) or later. |
| 21 | + |
| 22 | ++ [Azure Functions Core Tools](../functions-run-local.md) |
| 23 | + |
| 24 | ++ Azure account with an active subscription. [Create an account for free](https://azure.microsoft.com/free/?ref=microsoft.com&utm_source=microsoft.com&utm_medium=docs&utm_campaign=visualstudio). |
| 25 | + |
| 26 | ++ An HTTP test tool that keeps your data secure. For more information, see [HTTP test tools](../functions-develop-local.md#http-test-tools). |
| 27 | + |
| 28 | +## Create a local Durable Functions project |
| 29 | + |
| 30 | +In Visual Studio Code, [create a .NET isolated Durable Functions project](./quickstart-mssql.md) configured to use the MSSQL backend. Follow the article until the [Test locally](./quickstart-mssql.md#test-locally) section and make sure you can run the app locally before going to the next step. |
| 31 | + |
| 32 | +### Add Docker related files |
| 33 | + |
| 34 | +1. In the project root directory, add a _Dockerfile_ with the following content: |
| 35 | + ``` |
| 36 | + FROM mcr.microsoft.com/dotnet/sdk:8.0 AS installer-env |
| 37 | +
|
| 38 | + COPY . /src/dotnet-function-app |
| 39 | + RUN cd /src/dotnet-function-app && \ |
| 40 | + mkdir -p /home/site/wwwroot && \ |
| 41 | + dotnet publish *.csproj --output /home/site/wwwroot |
| 42 | +
|
| 43 | + # To enable ssh & remote debugging on app service change the base image to the one below |
| 44 | + # FROM mcr.microsoft.com/azure-functions/dotnet-isolated:4-dotnet-isolated8.0-appservice |
| 45 | + FROM mcr.microsoft.com/azure-functions/dotnet-isolated:4-dotnet-isolated8.0 |
| 46 | + ENV AzureWebJobsScriptRoot=/home/site/wwwroot \ |
| 47 | + AzureFunctionsJobHost__Logging__Console__IsEnabled=true |
| 48 | +
|
| 49 | + COPY --from=installer-env ["/home/site/wwwroot", "/home/site/wwwroot"] |
| 50 | + ``` |
| 51 | +1. Add a _.dockerignore_ file with the following content: |
| 52 | + ``` |
| 53 | + local.settings.json |
| 54 | + ``` |
| 55 | +
|
| 56 | +## Build the container image |
| 57 | +
|
| 58 | +The Dockerfile in the project root describes the minimum required environment to run the function app in a container. The complete list of supported base images for Azure Functions is documented above as Host images in the pre-requisites section or can be found in the [Azure Functions Base by Microsoft | Docker Hub](https://hub.docker.com/_/microsoft-azure-functions-base) |
| 59 | +
|
| 60 | +1. Start the Docker daemon. |
| 61 | +
|
| 62 | +1. Sign in to Docker with the [docker login](https://docs.docker.com/engine/reference/commandline/login/) command. This command prompts you for your username and password. A "Login Succeeded" message confirms that you're signed in. |
| 63 | +
|
| 64 | +1. In the **LocalFunctionProj** project folder, run the following command to build the image, replacing <DOCKER_ID> with your Docker Hub account ID: |
| 65 | + ```bash |
| 66 | + dockerId=<DOCKER_ID> |
| 67 | + imageName=IMAGE_NAME> |
| 68 | + imageVersion=v1.0.0 |
| 69 | +
|
| 70 | + docker build --platform linux --tag $dockerId/$imageName:$imageVersion . |
| 71 | + ``` |
| 72 | +
|
| 73 | + >[!NOTE] |
| 74 | + > If you're running on an M-series Mac, use `--platform linux/amd64` instead. |
| 75 | +
|
| 76 | +1. Push the image to Docker: |
| 77 | + ```bash |
| 78 | + docker push $dockerId/$imageName:$imageVersion |
| 79 | + ``` |
| 80 | +Depending on network speed, pushing the image the first time might take a few minutes (pushing subsequent changes is much faster). While you're waiting, proceed to the next section to create Azure resources in another terminal. |
| 81 | +
|
| 82 | +## Create Azure resources |
| 83 | +
|
| 84 | +The following instructions will guide you through creating these Azure resources: |
| 85 | +- Azure resource group: For convenient cleanup later, all resources are created in this group. |
| 86 | +- Azure Container App environment: Environment where container app is hosted. |
| 87 | +- Azure Container App: Image containing the Durable Functions app is deployed to this app. |
| 88 | +- Azure Storage Account: Required by the function app to store app related data such as application code. |
| 89 | +- A virtual network: Required by the Azure Container App environment. |
| 90 | +
|
| 91 | +### Initial set up |
| 92 | +
|
| 93 | +1. Login to your Azure subscription: |
| 94 | + ```azurecli |
| 95 | + az login |
| 96 | +
|
| 97 | + az account set -subscription | -s <subscription_name> |
| 98 | + ``` |
| 99 | +
|
| 100 | +1. Run the required commands to set up the Azure Container Apps CLI extension: |
| 101 | + ```azurecli |
| 102 | + az upgrade |
| 103 | +
|
| 104 | + az extension add --name containerapp --upgrade |
| 105 | +
|
| 106 | + az provider register --namespace Microsoft.App |
| 107 | +
|
| 108 | + az provider register --namespace Microsoft.OperationalInsights |
| 109 | + ``` |
| 110 | +
|
| 111 | +### Create container app and related resources |
| 112 | +
|
| 113 | +A workload profile determines the amount of compute and memory resources available to the container apps deployed in an environment. Create a Consumption workload profile for scale-to-zero support and pay-per-use. Learn more about [workload profiles](../functions-container-apps-hosting.md#hosting-and-workload-profiles). |
| 114 | +
|
| 115 | +```azurecli |
| 116 | +location=<REGION> |
| 117 | +resourceGroup=<RESOURCE_GROUP_NAME> |
| 118 | +storage=<STORAGE_NAME> |
| 119 | +containerAppEnv=<CONTAINER_APP_ENVIRONMNET_NAME> |
| 120 | +functionApp=<APP_NAME> |
| 121 | +vnet=<VNET_NAME> |
| 122 | +
|
| 123 | +# Create an Azure resource group |
| 124 | +az group create --name $resourceGroup --location $location |
| 125 | +
|
| 126 | +# A VNET is required when creating a container app environment |
| 127 | +az network vnet create --resource-group $resourceGroup --name $vnet --location $location --address-prefix 10.0.0.0/16 |
| 128 | +
|
| 129 | +# The VNET must have a subnet for the environment deployment |
| 130 | +az network vnet subnet create \ |
| 131 | + --resource-group $resourceGroup \ |
| 132 | + --vnet-name $vnet \ |
| 133 | + --name infrastructure-subnet \ |
| 134 | + --address-prefixes 10.0.0.0/23 |
| 135 | +
|
| 136 | +# Get subnet ID |
| 137 | +subnetId=$(az network vnet subnet show --resource-group $resourceGroup --vnet-name $vnet --name infrastructure-subnet --query "id" -o tsv | tr -d '[:space:]') |
| 138 | +
|
| 139 | +# Delegate the subnet to `Microsoft.App/environments` |
| 140 | +az network vnet subnet update --resource-group $resourceGroup --vnet-name $vnet --name infrastructure-subnet --delegations Microsoft.App/environments |
| 141 | +
|
| 142 | +# Create the container app environment |
| 143 | +az containerapp env create \ |
| 144 | + --enable-workload-profiles \ |
| 145 | + --resource-group $resourceGroup \ |
| 146 | + --name $containerAppEnv \ |
| 147 | + --location $location \ |
| 148 | + --infrastructure-subnet-resource-id $subnetId |
| 149 | +
|
| 150 | +# Create a container app based on the Durable Functions image |
| 151 | +az containerapp create --resource-group $resourceGroup \ |
| 152 | +--name $functionApp \ |
| 153 | +--environment $containerAppEnv \ |
| 154 | +--image $dockerId/$imageName:$imageVersion \ |
| 155 | +--ingress external \ |
| 156 | +--allow-insecure \ |
| 157 | +--target-port 80 \ |
| 158 | +--transport auto \ |
| 159 | +--kind functionapp \ |
| 160 | +--query properties.outputs.fqdn |
| 161 | +``` |
| 162 | + |
| 163 | +Note down the app link, which should look similar to: |
| 164 | +``` |
| 165 | +https://<APP_NAME>.victoriouswave-3866c33e.<REGION>.azurecontainerapps.io/ |
| 166 | +``` |
| 167 | + |
| 168 | +### Create databases |
| 169 | + |
| 170 | +Create an Azure Storage account as required by the function app: |
| 171 | + |
| 172 | +```azurecli |
| 173 | +az storage account create --name $storage --location $location --resource-group $resourceGroup --sku Standard_LRS |
| 174 | +``` |
| 175 | + |
| 176 | +Your Durable Functions also needs an Azure SQL Database as its storage backend. This is where state information is persisted as your orchestrations run. In the Azure portal, you can [create an Azure SQL database](/azure/azure-sql/database/single-database-create-quickstart). During creation: |
| 177 | +- Enable Azure services and resources to access this server (under _Networking_) |
| 178 | +- Set the value for _Database collation_ (under _Additional settings_) to `Latin1_General_100_BIN2_UTF8`. |
| 179 | + |
| 180 | +> [!NOTE] |
| 181 | +> Enabling the **Allow Azure services and resources to access this server** setting isn't a recommended security practice for production scenarios. Real applications should implement more secure approaches, such as stronger firewall restrictions or virtual network configurations. |
| 182 | +
|
| 183 | + |
| 184 | +### Configure identity-based authentication |
| 185 | + |
| 186 | +Managed identities make your app more secure by eliminating secrets from your app, such as credentials in the connection strings. You can choose between [system-assigned and user-assigned managed identity](/entra/identity/managed-identities-azure-resources/overview). This article demonstrates setting up user-assigned managed identity, which is the recommended option as it is not tied to the app lifecycle. |
| 187 | + |
| 188 | +1. Create identity and assign it to the container app: |
| 189 | + ```azurecli |
| 190 | + # Variables |
| 191 | + subscription=<SUBSCRIPTION_ID> |
| 192 | + identity=<IDENTITY_NAME> |
| 193 | +
|
| 194 | + # Create a managed identity resource |
| 195 | + echo "Creating $identity" |
| 196 | + az identity create -g $resourceGroup -n $identity --location "$location" |
| 197 | +
|
| 198 | + # Assign the identity to the container app |
| 199 | + echo "Assigning $identity to app" |
| 200 | + az containerapp identity assign --resource-group $resourceGroup --name $functionApp --user-assigned $identity |
| 201 | + ``` |
| 202 | +
|
| 203 | +1. Assign the identity `Storage Blob Data Owner` role for access to the storage account. |
| 204 | + ``` |
| 205 | + # Set the scope of the access |
| 206 | + scope="/subscriptions/$subscription/resourceGroups/$resourceGroup/providers/Microsoft.Storage/storageAccounts/$storage" |
| 207 | + |
| 208 | + # Get the identity's ClientId |
| 209 | + clientId=$(az identity show --name $identity --resource-group $resourceGroup --query 'clientId' --output tsv) |
| 210 | + |
| 211 | + # Assign the role |
| 212 | + echo "Assign Storage Blob Data Owner role to identity" |
| 213 | + az role assignment create --assignee "$clientId" --role "Storage Blob Data Owner" --scope "$scope" |
| 214 | + ``` |
| 215 | +
|
| 216 | +> [!NOTE] |
| 217 | +> Authenticating to the MSSQL database using managed identity isn't supported when hosting a Durable Functions app in Azure Container Apps. Use connection string for now. |
| 218 | +
|
| 219 | +### Set up app settings |
| 220 | +
|
| 221 | +1. Find the connection string by going to the SQL database resource on Azure portal, navigating to the **Settings** tab, then clicking on **Connection strings**: |
| 222 | + :::image type="content" source="./media/quickstart-mssql/mssql-azure-db-connection-string.png" alt-text="Screenshot showing database connection string."::: |
| 223 | +
|
| 224 | + The connection string should have this format: |
| 225 | + ```bash |
| 226 | + dbserver=<SQL_SERVER_NAME> |
| 227 | + sqlDB=<SQL_DB_NAME> |
| 228 | + username=<DB_USER_LOGIN> |
| 229 | + password=<DB_USER_PASSWORD> |
| 230 | +
|
| 231 | + connStr="Server=tcp:$dbserver.database.windows.net,1433;Initial Catalog=$sqlDB;Persist Security Info=False;User ID=$username;Password=$password;MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30;" |
| 232 | + ``` |
| 233 | +
|
| 234 | + If you forget the password from the previous database creation step, you can reset it on the SQL server resource: |
| 235 | + :::image type="content" source="./media/quickstart-mssql/mssql-azure-reset-pass-2.png" alt-text="Screenshot showing reset password button."::: |
| 236 | +
|
| 237 | +1. Store the SQL database's connection string as a [secret](../../container-apps/manage-secrets.md) called _sqldbconnection_ in the container app: |
| 238 | + ```azurecli |
| 239 | + az containerapp create \ |
| 240 | + --resource-group $resourceGroup \ |
| 241 | + --name $functionApp \ |
| 242 | + --environment $containerAppEnv \ |
| 243 | + --image $dockerId/$imageName:$imageVersion \ |
| 244 | + --secrets sqldbconnection=$connStr |
| 245 | + ``` |
| 246 | +
|
| 247 | +1. Add these settings to the app: |
| 248 | + - `AzureWebJobsStorage__accountName`: Azure Storage account name |
| 249 | + - `AzureWebJobsStorage__clientId`: ClientId of the managed identity |
| 250 | + - `AzureWebJobsStorage__credential`: Credential type, which is _managedidentity_ |
| 251 | + - `SQLDB_Connection`: Reference the SQL database connection string stored as a secret |
| 252 | + - `FUNCTIONS_WORKER_RUNTIME`: Programming language of the app |
| 253 | +
|
| 254 | + ```azurecli |
| 255 | + az containerapp update \ |
| 256 | + -n $functionApp \ |
| 257 | + -g $resourceGroup \ |
| 258 | + --set-env-vars SQLDB_Connection=secretref:sqldbconnection \ |
| 259 | + AzureWebJobsStorage__accountName=$storage \ |
| 260 | + AzureWebJobsStorage__clientId=$clientId \ |
| 261 | + AzureWebJobsStorage__credential=managedidentity \ |
| 262 | + FUNCTIONS_WORKER_RUNTIME=dotnet-isolated |
| 263 | + ``` |
| 264 | +
|
| 265 | +## Test |
| 266 | +
|
| 267 | +1. Use an HTTP test tool to send a POST request to the HTTP trigger endpoint, which should be similar to: |
| 268 | + ``` |
| 269 | + https://<APP NAME>.victoriouswave-3866c33e.<REGION>.azurecontainerapps.io/api/DurableFunctionsOrchestrationCSharp1_HttpStart |
| 270 | + ``` |
| 271 | +
|
| 272 | + The response is the HTTP function's initial result. It lets you know that the Durable Functions orchestration started successfully. It doesn't yet display the end result of the orchestration. The response includes a few useful URLs. |
| 273 | +
|
| 274 | +1. Copy the URL value for `statusQueryGetUri`, paste it in your browser's address bar, and execute the request. Alternatively, you can also continue to use the HTTP test tool to issue the GET request. |
| 275 | + The request queries the orchestration instance for the status. You should see that the instance finished and that it includes the outputs or results of the Durable Functions app like in this example: |
| 276 | +
|
| 277 | + ```json |
| 278 | + { |
| 279 | + "name":"HelloCities", |
| 280 | + "instanceId":"7f99f9474a6641438e5c7169b7ecb3f2", |
| 281 | + "runtimeStatus":"Completed", |
| 282 | + "input":null, |
| 283 | + "customStatus":null, |
| 284 | + "output":"Hello, Tokyo! Hello, London! Hello, Seattle!", |
| 285 | + "createdTime":"2023-01-31T18:48:49Z", |
| 286 | + "lastUpdatedTime":"2023-01-31T18:48:56Z" |
| 287 | + } |
| 288 | + ``` |
| 289 | +
|
| 290 | +## Next steps |
| 291 | +
|
| 292 | +Learn more about: |
| 293 | +- [Azure Container Apps hosting of Azure Functions](../functions-container-apps-hosting.md). |
| 294 | +- [MSSQL storage provider](https://microsoft.github.io/durabletask-mssql/) architecture, configuration, and workload behavior. |
| 295 | +- The Azure-managed storage backend, [Durable Task Scheduler](./durable-task-scheduler/durable-task-scheduler.md). |
0 commit comments