Azure Container Registry
Compatible with the azure-mgmt-containerregistry SDK, the az acr CLI, Terraform's
azurerm_container_registry, and any ARM-speaking client for the management plane, plus any
standard Docker client (docker login / push / pull, OCI tooling) for the data plane,
either anonymously on the registry container's published port or with an Entra token on
{name}.azurecr.io.
Features
- Registry lifecycle: create, get, update, delete; list by resource group and by subscription
- Admin credentials:
listCredentials/regenerateCredential(username + two passwords) - Name availability:
checkNameAvailability - Usages:
listUsages(static quota report) - Entra token exchange: the bearer challenge on
GET /v2/,POST /oauth2/exchangeand/oauth2/token, soaz acr loginand the Azure SDK container-registry clients work - Data plane: a single shared
registry:2sidecar exposing the Docker Registry HTTP API V2 (/v2/…), so images push and pull with the standard Docker client. All registries are backed by one container and isolated by an internal repository prefix ({registryName}/{repo})
Endpoint
Management plane (ARM) goes through port 4577:
PUT|GET|PATCH|DELETE /subscriptions/{s}/resourceGroups/{rg}/providers/Microsoft.ContainerRegistry/registries/{name}
POST .../registries/{name}/listCredentials | regenerateCredential | importImage
GET .../registries/{name}/listUsages
POST /subscriptions/{s}/providers/Microsoft.ContainerRegistry/checkNameAvailability
The data plane has two surfaces onto the same storage.
The registry container's published port is the zero-configuration one. It is plain HTTP,
anonymous, and Docker treats localhost:PORT as insecure automatically, so nothing has to be
configured:
The port comes from the base-port-max-port range below and is allocated when the shared
container starts, on the first registry you create. It is 5000 unless that port is taken, and
because loginServer names the Azure host it cannot carry the port, so the registry resource
reports it as properties.localPort:
This is a floci-az convenience field, not part of the real ARM contract, and it is the same field
PostgreSQL and MySQL servers use to report the port their container
published. It is absent in mocked mode, where no container runs. The emulator also logs the port
at startup, and docker port floci-az-acr-registry 5000 reports it:
INFO [io.flo.az.ser.acr.AcrRegistryManager] Started shared ACR registry floci-az-acr-registry on host port 5000
{name}.azurecr.io is what loginServer reports and what Azure-native clients use. It is
served by floci-az on 443 when TLS is on (FLOCI_AZ_TLS_ENABLED=true), and carries the endpoints
those clients need:
GET {name}.azurecr.io/v2/ bearer challenge
POST {name}.azurecr.io/oauth2/exchange Entra access token → ACR refresh token
GET|POST {name}.azurecr.io/oauth2/token ACR refresh token → scoped access token
* {name}.azurecr.io/v2/** Docker Registry HTTP API V2
The registry name is not part of a repository reference here: {name}.azurecr.io/app is the
repository app, exactly as in Azure. floci-az adds the {registryName}/ prefix internally when it
proxies to the shared container, and strips it again from upload Location headers, tags/list and
_catalog.
Name resolution is required.
{name}.azurecr.iomust resolve to floci-az, the same requirement*.vault.azure.netalready carries for Key Vault. Add a hosts entry (or point your resolver at the emulator) and trust the emulator's certificate, which carries*.azurecr.io:echo "127.0.0.1 myregistry.azurecr.io" | sudo tee -a /etc/hosts curl -s http://localhost:4577/_floci/tls-cert -o floci-az.crt # then trust itDocker needs the certificate in
/etc/docker/certs.d/myregistry.azurecr.io/ca.crt. Without name resolution, use the published port instead: it needs none of this.A hosts entry points one name at the emulator, but a wildcard resolver points all of them, so floci-az serves
{name}.azurecr.ioonly for a registry that was actually created. A name with no registry behind it answers404 NAME_UNKNOWNat every endpoint, challenge and token included, rather than minting tokens for something that does not exist.TLS is off by default. The challenge realm carries the scheme the request arrived with (honouring
X-Forwarded-Protobehind a terminating proxy), so the token flow is exercisable over plain HTTP, butaz acr loginand Docker both address a named registry ashttps://and need TLS on.
Authentication
az acr login and the SDK container-registry clients perform Azure's Entra token exchange rather
than a Docker login: GET /v2/ answers 401 with a Bearer challenge naming the realm and
service, an Entra access token is traded for a registry refresh token at /oauth2/exchange, and
that refresh token is traded for a scoped access token at /oauth2/token. Docker then logs in with
the username 00000000-0000-0000-0000-000000000000 and the access token as the password. floci-az
serves all of it, in both mocked modes.
Intentional deviations, all of them a consequence of the emulator's existing stance that ARM and Shared Key credentials are accepted without verification:
- Tokens are issued, not verified. Both tokens are real RS256 JWTs signed by the emulator's
Entra signing key, so clients that decode them keep working (the Azure CLI reads the access
token's
accessclaim). Nothing checks the Entra token presented at/oauth2/exchange, and nothing checks the refresh token presented at/oauth2/token. - No authorization is enforced. The requested scope is recorded in the access token's
accessclaim and then ignored: any token, and any identity, reaches every repository. OnlyGET /v2/challenges; repository paths are served whether or not a token is presented. Admin credentials are accepted at the token endpoint so thepasswordgrant is truthful, but they are not required. - The shared backing registry runs anonymous (mirroring the AWS ECR design in the sibling emulator), so its published port serves push and pull with no credentials at all.
Real ACR enforces per-registry auth and repository-scoped permissions; reproducing that on a single shared registry is out of scope.
Example
Anonymous, through the published port. 5000 below is the first free port in the configured range,
so substitute the one the startup log reports (Started shared ACR registry ... on host port):
docker tag busybox localhost:5000/myregistry/demo/busybox:v1
docker push localhost:5000/myregistry/demo/busybox:v1
curl http://localhost:5000/v2/_catalog # {"repositories":["myregistry/demo/busybox", ...]}
docker pull localhost:5000/myregistry/demo/busybox:v1
The same image, through the Azure login server (hosts entry and trusted certificate in place):
az acr create -n myregistry -g my-rg --sku Basic # loginServer: myregistry.azurecr.io
az acr login -n myregistry
docker tag busybox myregistry.azurecr.io/demo/busybox:v1
docker push myregistry.azurecr.io/demo/busybox:v1
docker pull myregistry.azurecr.io/demo/busybox:v1
az acr login --expose-token returns the refresh token without needing a Docker daemon.
In mocked mode (no Docker) the management plane and the whole auth surface still work;
repository operations answer with the registry's UNSUPPORTED error, since no container is running.
Configuration
floci-az:
services:
acr:
enabled: true
mocked: false # false (default) = one shared registry:2 for all registries. true = management plane only, no Docker
default-image: "registry:2"
base-port: 5000 # host port range start for registry containers
max-port: 5099 # host port range end
| Env var | Default | Description |
|---|---|---|
FLOCI_AZ_SERVICES_ACR_ENABLED |
true |
Enable/disable the service |
FLOCI_AZ_SERVICES_ACR_MOCKED |
false |
Mocked mode (management plane only, no Docker) |
FLOCI_AZ_SERVICES_ACR_DEFAULT_IMAGE |
registry:2 |
Registry container image |
FLOCI_AZ_SERVICES_ACR_BASE_PORT |
5000 |
Host port range start |
FLOCI_AZ_SERVICES_ACR_MAX_PORT |
5099 |
Host port range end |
Out of scope (future work)
- Token signature verification, repository-level authorization and scope enforcement.
- Scope maps and tokens as ACR resources, content trust, quarantine, anonymous pull configuration.
importImageactual layer copy (accepted as a202no-op).- Geo-replication, webhooks, ACR Tasks, private link, retention policies (accepted and echoed as static properties, not enforced).
- SKU behavioral differences (Basic/Standard/Premium accepted; no functional difference).