Raised: $0
0% of monthly goal Help us cross the finish line!
Goal: $12,000
Raised: $0 Goal: $12,000
0% of monthly goal Help us cross the finish line!
Sponsor DDEV

If you find this add-on useful, please star it on GitHub — stars show appreciation and help maintainers know their work matters.

ddev-floci-az

add-on registry tests last commit release

A DDEV add-on that runs floci-az — a free, open-source local Azure emulator — alongside your project, and points your web container at it.

Your application talks to http://floci-az:4577 instead of Azure. Blob containers, Queues, Tables, Key Vault secrets, App Configuration keys, Cosmos DB databases, Entra ID tokens and about twenty other services exist locally, with no Azure subscription, no service principal, and no billing account.

Why this add-on

Azurite, Microsoft’s official local emulator, covers Blob, Queue and Table and stops there — no Key Vault, no App Configuration, no Cosmos, no Entra, no ARM. floci-az covers all of those in one ~78 MB container on one port, MIT licensed, with no account and no feature gates. It is wire-compatible with Azurite, so the connection string you already have keeps working.

Running it as a DDEV add-on rather than as a separate docker compose stack buys you three things that are fiddly to arrange by hand:

Running alongside the AWS and GCP emulators

Everything this add-on installs is named for the cloud it emulates rather than for Floci: the service and its hostname are floci-az, the container is ddev-<project>-floci-az, the state volume is floci-az, and the command is ddev floci-az. Nothing here claims the bare floci name, so the equivalent AWS and GCP add-ons can be installed in the same project without overwriting this one’s files or shadowing its command.

Your application can then tell them apart by endpoint: http://floci-aws:4566, http://floci-az:4577, http://floci-gcp:4588.

Installation

ddev add-on get codementality/ddev-floci-az
ddev restart

Then check it:

ddev floci-az az storage container create --name uploads
ddev floci-az az storage container list -o table

Using it

Nothing in your application needs to know it is talking to an emulator. The web container already has the environment set:

Variable Value
AZURE_STORAGE_CONNECTION_STRING The full Azurite-style string, endpoints on floci-az:4577
AZURE_STORAGE_ACCOUNT, AZURE_STORAGE_KEY devstoreaccount1 and the well-known dev key
AZURE_STORAGE_ALLOW_HTTP true
AZURE_STORAGE_BLOB_ENDPOINT, ..._QUEUE_ENDPOINT, ..._TABLE_ENDPOINT Per-service URLs
AZURE_KEY_VAULT_ENDPOINT, AZURE_COSMOS_ENDPOINT, AZURE_APP_CONFIGURATION_ENDPOINT Per-service URLs
AZURE_AUTHORITY_HOST, AZURE_TENANT_ID, AZURE_CLIENT_ID, AZURE_CLIENT_SECRET The seeded Entra tenant and app registration
FLOCI_AZ_ENDPOINT http://floci-az:4577 — the bare root, for ARM calls

Only the connection string is read automatically; the rest are endpoints for your code to pass to a client constructor.

PHP (microsoft/azure-storage-blob or the newer azure-sdk-for-php):

$client = MicrosoftAzure\Storage\Blob\BlobRestProxy::createBlobService(
  getenv('AZURE_STORAGE_CONNECTION_STRING')
);
$client->createContainer('uploads');
$client->createBlockBlob('uploads', 'a.txt', 'hi');

For Drupal with a flysystem-backed module, point the container at uploads and hand it the same connection string.

Python / Node / .NET / Java: BlobServiceClient.from_connection_string(...) and its equivalents all take AZURE_STORAGE_CONNECTION_STRING as-is.

One port, many paths

Azure normally gives each service its own hostname. floci-az serves everything on port 4577 and routes by path prefix instead, which is why the connection string has three different endpoint URLs on the same host and port:

Service Path
Blob /devstoreaccount1
Queue /devstoreaccount1-queue
Table /devstoreaccount1-table
Key Vault /devstoreaccount1-keyvault
App Configuration /devstoreaccount1-appconfig
Cosmos DB /devstoreaccount1-cosmos
Functions /devstoreaccount1-functions
ARM (SQL, AKS, Redis, ACR, VNet …) /subscriptions/...

Dropping the -queue or -table suffix is the classic way to have Blob work and everything else return 404. The add-on builds all three correctly; if you write a connection string by hand, keep the suffixes.

The two default containers

Out of the box the add-on creates two Blob Storage containers on every start:

Container Default name Intent
Public public Created with --public-access blob
Private private No public access — owner only

The web container gets AZURE_PUBLIC_CONTAINER, AZURE_PRIVATE_CONTAINER, AZURE_STORAGE_CONTAINER (the public one) and AZURE_PUBLIC_CONTAINER_URL — the last being the router URL a browser can actually resolve, since a URL built from floci-az:4577 only works inside Docker.

Rename either, or set one to an empty string to skip it:

ddev dotenv set .ddev/.env.floci-az --floci-az-public-container=assets
ddev dotenv set .ddev/.env.floci-az --floci-az-private-container=""
ddev restart

How they get created, since floci-az has no init hooks

The AWS and GCP emulators run initialization scripts from a directory inside the container. floci-az has no such mechanism — there is no /etc/floci-az/init — so this add-on creates the containers from a DDEV post-start hook instead, wired up in .ddev/config.floci-az.yaml and running .ddev/floci-az/create-containers.sh.

That is worth knowing if you want to add your own startup resources: put them in that script, or add another post-start hook of your own. az storage container create is already idempotent, so re-running on every start is safe.

Container permissions are not enforced

The two containers are equally open at runtime.

So the split exists to make your application and IaC exercise the right shapes — public assets in one container, private uploads in the other, with the right publicAccess in your Bicep or Terraform — and so it is already in place if upstream starts enforcing it. Treat both containers as world-readable locally, and do not rely on the emulator to catch a permissions mistake; only real Azure will.

Unlike GCS on floci-gcp, container and blob listing here does survive a restart correctly.

Measured against floci/floci-az:0.11.0 on 2026-08-26. Floci is under active development, so this may already be out of date. The add-on’s test suite carries a canary — container permissions are advisory only, and listing survives a restart — that goes red when this changes, so a nightly CI failure there is good news, not a regression.

Key Vault, Entra ID and the auth story

Storage runs in dev auth mode, which accepts any credential and any signature — that is what makes the emulator usable without real keys.

Key Vault is the exception. It requires a real bearer token even locally, because that is how the Key Vault API works. floci-az ships a full local Entra ID that issues genuine RS256-signed JWTs from a seeded tenant, so the fix is to ask it for one — and the web container already has everything needed:

TOKEN=$(curl -s -X POST \
  -d "grant_type=client_credentials&client_id=${AZURE_CLIENT_ID}&client_secret=${AZURE_CLIENT_SECRET}&scope=https://vault.azure.net/.default" \
  "${AZURE_AUTHORITY_HOST}${AZURE_TENANT_ID}/oauth2/v2.0/token" \
  | sed -n 's/.*"access_token":"\([^"]*\)".*/\1/p')

curl -X PUT -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"value":"s3cret"}' \
  "${AZURE_KEY_VAULT_ENDPOINT}/secrets/api-key?api-version=7.4"

In an SDK, ClientSecretCredential with those three variables does the same thing, and DefaultAzureCredential picks them up from the environment unassisted — AZURE_AUTHORITY_HOST is the variable that redirects it away from login.microsoftonline.com. The seeded values are tenant 00000000-0000-0000-0000-000000000002, client 11111111-1111-1111-1111-111111111111, secret floci-az-dev-secret.

Set --floci-az-auth-mode=strict if you want storage signatures validated too.

App Configuration needs an https endpoint

The App Configuration SDK refuses a plaintext endpoint outright, and floci-az speaks http on 4577. So AZURE_APP_CONFIGURATION_ENDPOINT is deliberately set to an https:// URL, and the convention is a transport that rewrites the scheme on the way out:

from azure.appconfiguration import AzureAppConfigurationClient
from azure.core.pipeline.transport import RequestsTransport

class ForceHttpTransport(RequestsTransport):
    def send(self, request, **kwargs):
        request.url = request.url.replace("https://", "http://", 1)
        return super().send(request, **kwargs)

conn = f"Endpoint={os.environ['AZURE_APP_CONFIGURATION_ENDPOINT']};Id=devstoreaccount1;Secret=placeholder"
client = AzureAppConfigurationClient.from_connection_string(conn, transport=ForceHttpTransport())

Java takes the equivalent HttpPipelinePolicy. Plain HTTP calls to ${FLOCI_AZ_ENDPOINT}/devstoreaccount1-appconfig/kv/... need none of this.

Reaching Floci from the host

Two endpoints, and neither needs a published host port:

ddev floci-az url
# From the project (web container, Functions, SQL): http://floci-az:4577
# From the host (az CLI, Terraform, your IDE):      http://<project>.ddev.site:4577
#                                            (TLS: https://<project>.ddev.site:4579)

The host endpoint is ddev-router, which routes by hostname — so ports 4577 and 4579 are shared between every project running this add-on rather than owned by one of them, and each project’s requests reach its own emulator.

The plain-HTTP port matters more than it looks. An Azure connection string carries its own scheme, and the local-development convention every SDK and tutorial uses is DefaultEndpointsProtocol=http. A stable http endpoint means the connection string never has to fight certificate validation, and never has to change:

eval "$(ddev floci-az env)"
az storage container list -o table

Everything floci-az serves on 4577 is REST, so the router carries all of it — there is no direct host port to manage. (The GCP add-on does publish one, because gRPC does not survive Traefik; Azure has no equivalent need.)

Or skip the question entirely with ddev floci-az az ..., which runs the CLI inside the container.

Running several projects at once

Two DDEV projects with this add-on installed start side by side with no configuration — nothing claims a fixed host port, so there is nothing to collide. State is per project: the floci-az volume is namespaced ddev-<project>_floci-az, and FLOCI_AZ_DOCKER_RESOURCE_NAMESPACE scopes every sidecar container Floci spawns — a SQL Server, a Redis cache — to its own project. ddev floci-az reset or a full ddev delete in one leaves the other untouched.

Persistence

Upstream defaults to memory; this add-on defaults to hybrid — in-memory speed with an asynchronous flush every five seconds, so your containers, queues and secrets survive ddev restart. State lives in a Docker volume, not in your project directory.

# Wipe everything in place, no restart — the one for between test runs.
ddev floci-az flush

# Delete the state volume and any containers Floci spawned.
ddev floci-az reset

# Or make every start clean.
ddev dotenv set .ddev/.env.floci-az --floci-az-storage-mode=memory
ddev restart

Other modes are persistent (flush on graceful shutdown) and wal (write-ahead log, maximum durability). Individual services can override the global mode — --floci-az-storage-services-blob-mode=wal, say. Use memory in CI.

Docker-backed services and the Docker socket

floci-az backs Azure Functions, the Cosmos DB engines (MongoDB, PostgreSQL, Cassandra), Azure SQL, PostgreSQL and MySQL Flexible Server, Cache for Redis, Container Registry and AKS with real containers rather than mocking their responses — a real SQL Server, a real Valkey, a real k3s cluster, your actual function code executing. Doing so requires the host’s Docker socket, which docker-compose.floci-az-docker.yaml mounts. The socket is also what powers Floci’s embedded DNS.

Mounting the Docker socket into a container gives it control of the host’s Docker daemon, which is effectively root on the host. It is the same bargain LocalStack and Testcontainers ask for, but it is worth making deliberately. If this project only uses the in-process services — Blob, Queue and Table Storage, Key Vault, App Configuration, Cosmos NoSQL, Entra ID, Managed Identity, Event Grid, Monitor, APIM, Email, and the whole ARM management plane — you can decline it:

rm .ddev/docker-compose.floci-az-docker.yaml
ddev restart

You can also keep the socket and stop one service from spawning anything, by putting it in control-plane-only mock mode:

ddev dotenv set .ddev/.env.floci-az --floci-az-services-functions-mocked=true

Event Hubs, Service Bus and Virtual Machines ship mocked already — Event Hubs because its AMQP data plane does not yet work with the Azure SDKs upstream — so only the services listed above spawn containers out of the box.

One wrinkle worth knowing: containers Floci starts are its own, not DDEV’s, so ddev stop leaves them running. ddev floci-az reset removes the ones belonging to this project along with the state.

Azure SQL and the EULA

Microsoft’s SQL Server image will not start until its licence is accepted, so Azure SQL is ARM-state-only — you can create servers and databases, but nothing speaks TDS — until you say yes:

ddev dotenv set .ddev/.env.floci-az --floci-az-services-sql-accept-eula=Y
ddev restart

Note that the value has to be a real Y or N. floci-az refuses to boot at all if it is set to an empty string, which is why this add-on defaults it to N rather than leaving it unset.

Once a server exists, its container binds a port chosen by the OS; GET /devstoreaccount1-sql/servers/<name>/connect reports it. The same pattern applies to the Cosmos engines and AKS — see the ports reference.

Migrating from Azurite

floci-az is wire-compatible, so an existing setup mostly moves across unchanged:

The differences worth knowing: Azurite gives each service its own port (10000/10001/10002), floci-az gives them one port and three paths; and Azurite has no auth mode to speak of, where floci-az has dev and strict.

Configuration

These are Floci’s own variable names, not add-on-specific aliases, because floci-az already namespaces everything under FLOCI_AZ_*. Anything in the upstream reference works here verbatim — set it and restart:

ddev dotenv set .ddev/.env.floci-az --floci-az-services-cosmos-enabled=false
ddev restart

The ones this add-on gives a different default to, or invents:

Variable Default Purpose
FLOCI_AZ_IMAGE floci/floci-az:latest-compat Image tag. floci/floci-az:latest is the lean ~78 MB native build without the Azure CLI
FLOCI_AZ_HOSTNAME floci-az Hostname on DDEV’s network, and the host in returned URLs
FLOCI_AZ_BASE_URL http://floci-az:4577 Base URL Floci embeds in API responses (upstream: http://localhost:4577)
FLOCI_AZ_HTTP_PORT 4577 ddev-router port (http), shared between projects
FLOCI_AZ_HTTPS_PORT 4579 ddev-router port (https), shared between projects
FLOCI_AZ_ACCOUNT_NAME devstoreaccount1 Storage account in the connection string
FLOCI_AZ_ACCOUNT_KEY the well-known dev key Account key in the connection string
FLOCI_AZ_PUBLIC_CONTAINER public Blob container created on every start, public-access requested. Empty string to skip
FLOCI_AZ_PRIVATE_CONTAINER private Private blob container created on every start. Empty string to skip
FLOCI_AZ_STORAGE_MODE hybrid memory, persistent, hybrid or wal (upstream: memory)
FLOCI_AZ_AUTH_MODE dev dev accepts anything; strict validates HMAC-SHA256
FLOCI_AZ_SERVICES_DOCKER_NETWORK ddev-<project>_default Network for containers Floci spawns
FLOCI_AZ_DOCKER_RESOURCE_NAMESPACE <project> Names and labels spawned containers and volumes
FLOCI_AZ_SERVICES_SQL_ACCEPT_EULA N Y to accept Microsoft’s SQL Server licence
FLOCI_AZ_DNS_CONTAINER_FALLBACK_ENABLED true Public resolvers in spawned containers; set false on an offline network

Anything else — FLOCI_AZ_SERVICES_REDIS_DEFAULT_IMAGE, say — can go straight into .ddev/.env.floci-az and be added to the environment: block of .ddev/docker-compose.floci-az.yaml, or set in a compose file of your own.

The ddev floci-az command

Command Does
ddev floci-az az <args> Runs the Azure CLI inside the container, connection string already injected
ddev floci-az connstring [--host] Prints the connection string, in-project or host-side
ddev floci-az env [--unset] Prints export lines for your shell; eval "$(...)" to apply
ddev floci-az url Prints all three endpoints (--internal / --host / --router for one)
ddev floci-az health Hits /health
ddev floci-az accounts Lists the storage accounts that exist
ddev floci-az logs [-f] Container logs
ddev floci-az shell A shell in the container
ddev floci-az flush Wipes all emulated state in place, no restart
ddev floci-az reset Deletes the state volume, and any containers Floci spawned for this project

ddev floci-az az needs the Azure CLI, which only the -compat image ships. If you switch FLOCI_AZ_IMAGE to floci/floci-az:latest for the smaller image, use your own az after eval "$(ddev floci-az env)" instead.

The connection string is injected here rather than through azfloci, the wrapper the compat image ships. azfloci does the same job, but it also exports REQUESTS_CA_BUNDLE="", which current Azure CLI versions reject outright, so every command through it fails.

Removing the add-on

Clean up first, remove second. ddev add-on remove deletes the ddev floci-az command along with everything else, so reset is no longer available once the add-on is gone:

ddev floci-az reset          # containers Floci spawned, plus the state volume
ddev add-on remove floci-az

ddev add-on remove deliberately leaves two things behind, because neither is the add-on’s to throw away:

If you skipped the reset and the add-on is already gone, the leftover containers Floci spawned can be cleared by hand. Keep the namespace filter — the emulator label alone matches every project’s floci-az containers, not just this one’s:

docker rm -f $(docker ps -aq \
  --filter label=floci_emulator=floci-az \
  --filter label=floci_namespace=<project>)

Licensing

Two different licences are in play here, and it is worth being explicit about which covers what:

No Floci source is vendored in this repository. The add-on only references the published floci/floci-az image, which Docker pulls at runtime; nothing here redistributes Floci code or binaries. If you redistribute this add-on you are redistributing Apache-2.0 material only.

Credits

floci-az is by the floci-io project, MIT licensed. This add-on packages it under Apache 2.0; see Licensing above.

Behavioural notes in this README were verified against floci/floci-az:0.11.0 on 2026-08-26. Upstream ships frequently; the nightly workflow in .github/workflows/tests.yml re-runs the suite against latest every day, which is what catches drift.

Contributed and maintained by @codementality

If you find this add-on useful, please star it on GitHub — stars show appreciation and help maintainers know their work matters.