In the last tutorial we set up the base Docker development Image that we will use to develop Azure Function Applications.

Function Apps are serverless services that can run your code and then scale down so that you can fully optimise your cost to productivity ratio in Azure. For more details and for reference throughout this tutorial checkout the Microsoft documentation on Azure Functions

Microsoft provides a handy local development tool so that you can develop Function Apps locally, which can be exceptionally useful when you need to debug quickly and save cash before deploying to the cloud.

In Part 1 of this series, the example Docker Image we created contained the setup for this local development tool. You can verify it has installed correctly using the following command once we have a terminal on the container,

func help

At this point, if you wanted to, you can now quickly put in an example Azure Function and run it locally with a simple func start command. However, you may get some warnings about storage, and you also wont be able to utilise some of the more advanced features of Function Applications.

By the end of this tutorial you will,

  • Explore developing Azure Functions and Azure Durable Functions
  • Learn how to emulate Azure Cloud Storage locally using Docker.
  • Deploy a locally hosted Durable Azure Function

This tutorial will use Python, however Function Apps support a wide variety of programming languages if you prefer to use another.

This tutorial also assumes you have worked with Azure Functions before, if you haven’t try out this tutorial from Microsoft to get started.

Functions Vs Durable Functions

Ignoring all the different billing tiers of Azure Function Application you can get, there are two main types of Azure Function.

  • Azure Function Applications
  • Durable Azure Function Applications

Another thing to keep in mind is Azure Functions are serverless, they lean on Azure Storage account as a secondary resource to actually store the source code they run and artefacts from runtime when they execute.

One of the primary limitations when using an Azure Function is its compute, in both the amount of compute you have and how long you have that compute for.

Durable Functions are a way to get around this by dividing up workloads into their own dedicated functions to share the load and keep overall costs to a minimum without having fork out for a tier with higher compute power.

There are three primary components of a Durable Function.

Connecting from VS Code using Container Tools

  1. The Trigger - A trigger is the exact same as a regular Azure Function, it can be anything that will trigger the Functions code, including but not limited to, HTTP events, Schedules and other events from Azure resources.

  2. The Orchestrator - This is the component that defines what work needs to be done and distributes it out to the Activity Functions that will actually do the work.

  3. The Activity - This component does heavy lifting, spinning up, executing and passing back its state or completed data to the Orchestrator. An Orchestrator can spin up as many of these as needed.

As mentioned previously, Azure Functions require a Storage Account to operate. This is especially important for Durable Functions, which use several Storage Account features to persist and track orchestration state, activity execution, checkpoints, and messages, enabling all components to coordinate reliably and work together seamlessly. For full details on this check out the Microsoft Documentation

We could just spin up some cloud storage and point our locally developed Function at that, but this defeats the purpose of local development! Luckily there is a solution.

Adding Local ‘Cloud’ Storage

Microsoft has anticipated Developers desire to debug and develop code locally and have provided an open source tool to emulate Azure Storage, known as Azurite

This tool is written in Node.js but also comes pre-packaged for us in a Docker Image so we can quickly and easily deploy it to our develpment stack we started before.

To add this to the stack, simply edit the docker-compose.yaml file that we have started back in Part 1.

Your docker-compose.yaml file should now look like the one below,

services:
    dev-container:
        image: my-dev-img
        container_name: example-dev-container
        volumes:
            - .:/development
        networks:
            - devnet
        tty: true
        stdin_open: true

    dev-azurite:
        image: mcr.microsoft.com/azure-storage/azurite
        container_name: dev-azurite
        ports:
            - "10000:10000"
            - "10001:10001"
            - "10002:10002"
        command: azurite --blobHost 0.0.0.0 --queueHost 0.0.0.0 --tableHost 0.0.0.0
        networks:
            - devnet

networks:
    devnet:
        driver: bridge

The key changes we have made in this file from Part 1 are,

  1. We have created a Docker Network and added it to our dev-conainer
  2. We have created an Azurite Container called dev-azurite
    • Exposed ports
      • 10000 for Blobs
      • 10001 for Queues
      • 10002 for Tables
    • Ran a command to start Blob, Queue and Table hosts on the Container

Once this file is in place, you can bring it up on Docker using the following command

docker compose -p azure-dev-stack up -d

Note that this time we have added -p which stands for ‘Project Name’ this will name our stack of Docker Containers so that they are easier to track.

Homing in the Config

Now that our Containers are up, its time to connect to the Container as we did before in Part 1 of this series by attaching to the running Container using VS Code Containers extension.

Once connected create a new file called local.settings.json and put the following content in it,

{
    "IsEncrypted": false,
    "Values": {
        "FUNCTIONS_WORKER_RUNTIME": "python",
        "AzureWebJobsStorage": "UseDevelopmentStorage=true;DevelopmentStorageProxyUri=http://dev-azurite"
    }
}

This local settings config file will be used by the Func utility to configure the local running of your Azure Function.

As this is a local development environment, we set encryption to false (as its this is a development enviorment).

We also state the worker runtime, in this case Python.

Finally, most importantly we instruct Func to use our local development storage, and pass it the URI for our Azurite Docker container.

⚠️ WARNING: The local.settings.json has been included in this project / GitHub repository for the sake of instruction. Never commit this file to your projects and always include it in your .gitignore and .funcignore

Bringing it Together

Now that everything is set up, we can get developing!

This tutorial will use Python, and if you have done any amount of Python development you will appreciate it can be quite annoying juggling Python versions, libraries and virtual environments.

To get around this, I recommend using UV from Astral. This is a nice lightweight tool that makes doing the above a lot easier, and I made sure it was installed in the example Docker Image used in this tutorial. However, if you are comfortable with Python and getting a virtual environment set up, using this is optional.

Python Environment

It is important when developing with Python to create a virtual environment before beginning a project and install any libraries you need within this virtual environment.

Using UV you can do this with the following commands.

# Creates Virtual Environment with Python 3.13
uv venv --python 3.13 \

# Activates venv
source .venv/bin/activate \

# install required packages
uv pip install azure-functions azure-functions-durable requests

Its also a good time at this point to make sure that within your Container attached VS Code window, that the Python VS Code extension is installed. This will allow you to get syntax highlighting and can let you point VS Code at your virtual enviorment to check for missing libraries.

Connecting from VS Code using Container Tools

The code

For this tutorial I will provide an example Durable Function Application for you to test.

This application takes a HTTP POST request with a payload of an array of URLs. This array will be orchestrated into different actions to check if each URL is reachable.

Save the below in a file called function_app.py

import azure.functions as func
import azure.durable_functions as df
import requests

myApp = df.DFApp(http_auth_level=func.AuthLevel.ANONYMOUS)

# HTTP-triggered starter for the durable orchestrator.
# Expects a JSON body like: { "urls": ["https://example.com", "https://contoso.com"\] }
@myApp.route(route="orchestrators/{functionName}")
@myApp.durable_client_input(client_name="client")
async def http_start(req: func.HttpRequest, client):
    function_name = req.route_params.get('functionName')
    try:
        body = req.get_json()
    except ValueError:
        body = {}

    urls = body.get("urls") or []
    if not isinstance(urls, list):
        urls = []

    instance_id = await client.start_new(function_name, client_input={"urls": urls})
    response = client.create_check_status_response(req, instance_id)
    return response

# Orchestrator that iterates through a list of URLs and delegates reachability checks to an activity.
@myApp.orchestration_trigger(context_name="context")
def check_urls_orchestrator(context):
    input_data = context.get_input() or {}
    urls = input_data.get("urls") or []
    results = []

    for url in urls:
        result = yield context.call_activity("check_url", url)
        results.append(result)

    return results

# Activity that performs a simple HTTP request to determine whether the URL is reachable.
@myApp.activity_trigger(input_name="url")
def check_url(url: str):
    try:
        response = requests.get(url, timeout=10)
        reachable = response.status_code < 400
        status = "available" if reachable else "not available"
        return {"url": url, "available": reachable, "status": status, "status_code": response.status_code}
    except requests.RequestException:
        return {"url": url, "available": False, "status": "not available"}

Testing

Now that everything is ready, with the Python venv activated and the function_app.py and local.settings.json in place. We can now start our local function with the following command;

func start

You should now see the output Functions that are now running locally and VS Code should expose the port 7071 on your host so that you can access the Function App. Connecting from VS Code using Container Tools

To activate our function we need to send a POST request with a URL payload to the http_start function in this case the URL will be;

http://localhost:7071/api/orchestrators/check_urls_orchestrator

To do this I reccomend using a tool like Postman which can be installed on VS Code as an extension, or as a desktop application

Simply paste the URL above into the interface for Postman, set the body of the request to raw JSON, and populate it with some URLs to check.

Connecting from VS Code using Container Tools

Here is an example payload

{
    "urls": ["https://example.com", "https://invalid.example"]
}

Next simply send the payload and await the response.

The response will initially look quite messy, what we get is a series of URIs that correspond to data from our Durable Function, the one where we get the results of the execution is statusQueryGetUri.

Connecting from VS Code using Container Tools

Simply click on this URL and you will be directed to the results,

Connecting from VS Code using Container Tools

Thats a wrap ! You have successfully set up a local Docker development enviorment for developing Azure Durable Functions.

As per usual, if you want the files used in this project for your reference, you can find them here on my GitHub