ECR
Protocol: JSON 1.1 (X-Amz-Target: AmazonEC2ContainerRegistry_V20150921.*) for the control plane.
Data plane: OCI Distribution Spec v2 (/v2/...), proxied by Floci to a real registry:2 container.
Endpoint: POST http://localhost:4566/ for the control plane; <account>.dkr.ecr.<region>.localhost:4566/<repo> for docker push / docker pull.
Supported Actions
| Action | Description |
|---|---|
CreateRepository |
Create a new repository (lazy-starts the backing registry on first call) |
DescribeRepositories |
List repositories or fetch by name |
DeleteRepository |
Delete a repository (with force=true semantics for non-empty repos) |
GetAuthorizationToken |
Returns a docker-login token + proxy endpoint |
ListImages |
Enumerate tags and digests in a repository |
DescribeImages |
Image metadata: digest, size, push timestamp, manifest media type |
BatchGetImage |
Fetch image manifests, honoring acceptedMediaTypes |
BatchDeleteImage |
Delete images by tag or digest |
PutImageTagMutability |
Set tag mutability and reject replacement pushes to immutable tags |
TagResource / UntagResource / ListTagsForResource |
Resource tagging |
PutLifecyclePolicy / GetLifecyclePolicy / DeleteLifecyclePolicy |
Lifecycle policy round-trip (stored, not enforced) |
SetRepositoryPolicy / GetRepositoryPolicy / DeleteRepositoryPolicy |
Repository policy round-trip (stored, not enforced) |
Admin Endpoints
| Endpoint | Description |
|---|---|
POST /_floci/ecr/gc |
Run garbage collection on the backing registry:2 container to reclaim disk after image deletions |
Emulation Behavior
- Real OCI registry backing. A single shared
registry:2container per Floci instance stores all repositories. Floci proxies Docker Distribution traffic to it and starts it lazily on the first ECR API call. The container is reused across Floci restarts (keep-running-on-shutdown: trueby default), so pushed image bytes survive restarts. - Storage cleanup.
DeleteRepository --forceremoves repository manifests, including untagged manifests. When it deletes the final repository, or when a non-retained runtime stops, Floci removes the named registry volume only inmemorymode or whenprune-volumes-on-delete: true. - Loopback URI scheme. Repository URIs follow
<account>.dkr.ecr.<region>.localhost:<flociPort>/<repoName>. RFC 6761 reserves*.localhostto resolve to the loopback address, and the docker daemon auto-trusts loopback as an insecure registry, sodocker pushanddocker pullwork without daemon configuration. ApathURI style fallback (localhost:<flociPort>/<account>/<region>/<repo>) is available viafloci.services.ecr.uri-style: pathfor environments where*.localhostresolution misbehaves. - Image tag mutability.
IMMUTABLErepositories allow the first manifest write for a tag and reject every replacement, including a replacement with the same manifest. The proxy forwards blob uploads and digest-addressed manifests unchanged. - Authorization.
GetAuthorizationTokenreturnsBase64("AWS:floci")plus a proxy endpoint. The backingregistry:2runs without auth, so anyaws ecr get-login-password | docker loginsucceeds. - Manifest format negotiation.
BatchGetImageforwards the caller'sacceptedMediaTypesas the upstreamAcceptheader. Modern OCI manifests (application/vnd.oci.image.manifest.v1+json) and Docker v2 schema 2 are both supported. - Cross-account / cross-region isolation. Internally the registry namespaces repositories as
<account>/<region>/<repoName>, so the same repository name in different accounts or regions cannot collide. - Reconcile on first start. When the registry container starts, Floci queries
GET /v2/_catalogand recreatesRepositorymetadata entries for any namespaces present in the registry but missing from local storage. This means image bytes are never orphaned across restarts. - Upgrade from direct registry access. Floci recreates an older backing container with the same volume and a loopback-only port binding. Repositories written through the former direct endpoint retain their physical paths and map to the default account and Region; their returned URI moves to port
4566. - Lambda and ECS integration. Image-backed Lambda functions (
PackageType=Image) and ECS task containers reference the same loopbackrepositoryUri. Floci rewrites real-AWS-shaped<account>.dkr.ecr.<region>.amazonaws.com/...URIs to the loopback registry at pull time, so CDK'sDockerImageFunction(which generates AWS-shaped URIs in CloudFormation templates) works without any user-side rewriting. - Locally built images win. An AWS-shaped URI that names an image already present on the Docker daemon is used as-is instead of being rewritten (
FLOCI_SERVICES_ECR_PREFER_LOCAL_IMAGES, defaulttrue). Build the image under the exact URI your function or task definition declares (docker build -t <account>.dkr.ecr.<region>.amazonaws.com/<repo>:<tag> .) and it runs without a push to the emulated registry, and without the registry container being started. The check uses the reference Floci would launch, so withFLOCI_DOCKER_IMAGE_REGISTRY_BASEset the image must be present as<base>/<account>.dkr.ecr.<region>.amazonaws.com/<repo>:<tag>.
Configuration
| Variable | Default | Description |
|---|---|---|
FLOCI_SERVICES_ECR_ENABLED |
true |
Enable the ECR control plane and lazy registry start |
FLOCI_SERVICES_ECR_REGISTRY_IMAGE |
registry:2 |
Backing OCI registry image |
FLOCI_SERVICES_ECR_REGISTRY_CONTAINER_NAME |
floci-ecr-registry |
Container name used for idempotent reuse across restarts |
FLOCI_SERVICES_ECR_REGISTRY_BASE_PORT |
5100 |
First private loopback port for the backing registry |
FLOCI_SERVICES_ECR_REGISTRY_MAX_PORT |
5199 |
Last private loopback port for the backing registry |
FLOCI_SERVICES_ECR_DATA_PATH |
./data/ecr |
Bind-mount root for the registry data directory |
FLOCI_SERVICES_ECR_KEEP_RUNNING_ON_SHUTDOWN |
true |
Leave the registry container running so the next Floci start adopts it |
FLOCI_SERVICES_ECR_URI_STYLE |
hostname |
hostname = *.dkr.ecr.<region>.localhost; path = localhost:<port>/<account>/<region>/<repo> |
FLOCI_SERVICES_ECR_PREFER_LOCAL_IMAGES |
true |
Use an AWS-shaped ECR image URI as-is when the Docker daemon already has that image, instead of rewriting it to the loopback registry |
FLOCI_SERVICES_ECR_TLS_ENABLED |
false |
Reserved for future ACM-backed TLS |
Docker Compose port mapping
ECR uses Floci's existing 4566 listener. The backing registry binds a loopback-only implementation port, so no ECR range belongs in docker-compose.yml:
# ECR uses the existing Floci API port
services:
floci:
image: floci/floci:latest
ports:
- "4566:4566"
- "6379-6399:6379-6399" # ElastiCache
- "7001-7099:7001-7099" # RDS
volumes:
- /var/run/docker.sock:/var/run/docker.sock
docker login 000000000000.dkr.ecr.us-east-1.localhost:4566 works once Floci starts the registry sidecar.
Examples
export AWS_ENDPOINT=http://localhost:4566
# Create a repository
aws ecr create-repository \
--repository-name floci-it/app \
--endpoint-url $AWS_ENDPOINT
# {
# "repository": {
# "repositoryArn": "arn:aws:ecr:us-east-1:000000000000:repository/floci-it/app",
# "repositoryUri": "000000000000.dkr.ecr.us-east-1.localhost:4566/floci-it/app",
# "imageTagMutability": "MUTABLE",
# ...
# }
# }
# Authenticate stock docker against the emulated registry
aws ecr get-login-password --endpoint-url $AWS_ENDPOINT \
| docker login --username AWS --password-stdin \
000000000000.dkr.ecr.us-east-1.localhost:4566
# Push an image
docker pull alpine:3.19
docker tag alpine:3.19 \
000000000000.dkr.ecr.us-east-1.localhost:4566/floci-it/app:v1
docker push 000000000000.dkr.ecr.us-east-1.localhost:4566/floci-it/app:v1
# Inspect via the AWS CLI
aws ecr list-images --repository-name floci-it/app --endpoint-url $AWS_ENDPOINT
aws ecr describe-images --repository-name floci-it/app --endpoint-url $AWS_ENDPOINT
# Pull from a clean local image store
docker rmi 000000000000.dkr.ecr.us-east-1.localhost:4566/floci-it/app:v1
docker pull 000000000000.dkr.ecr.us-east-1.localhost:4566/floci-it/app:v1
# Use the image as a Lambda function
aws lambda create-function \
--function-name my-image-fn \
--package-type Image \
--code ImageUri=000000000000.dkr.ecr.us-east-1.localhost:4566/floci-it/app:v1 \
--role arn:aws:iam::000000000000:role/lambda-role \
--endpoint-url $AWS_ENDPOINT
aws lambda invoke --function-name my-image-fn /tmp/out.json --endpoint-url $AWS_ENDPOINT
# Tear down
aws ecr batch-delete-image --repository-name floci-it/app \
--image-ids imageTag=v1 --endpoint-url $AWS_ENDPOINT
aws ecr delete-repository --repository-name floci-it/app --force \
--endpoint-url $AWS_ENDPOINT
Pulling from other containers (EKS, same-network consumers)
The localhost-based repository URI only works from the host. Floci EKS
configures a containerd mirror to reach Floci's data plane, so Helm charts can reference the
pushed URI as-is.
SDK Example (Java)
EcrClient ecr = EcrClient.builder()
.endpointOverride(URI.create("http://localhost:4566"))
.region(Region.US_EAST_1)
.credentialsProvider(StaticCredentialsProvider.create(
AwsBasicCredentials.create("test", "test")))
.build();
// Create a repository
Repository repo = ecr.createRepository(req -> req.repositoryName("floci-it/app"))
.repository();
// Get a docker login token
GetAuthorizationTokenResponse auth = ecr.getAuthorizationToken();
AuthorizationData data = auth.authorizationData().get(0);
String decoded = new String(Base64.getDecoder().decode(data.authorizationToken()));
// decoded = "AWS:floci" → pipe to `docker login --username AWS --password-stdin <proxyEndpoint>`
// List images after a docker push
ListImagesResponse images = ecr.listImages(req -> req.repositoryName("floci-it/app"));
images.imageIds().forEach(System.out::println);
// Force-delete the repository
ecr.deleteRepository(req -> req.repositoryName("floci-it/app").force(true));
Using with AWS CDK
CDK's DockerImageFunction works against Floci unchanged:
import * as lambda from 'aws-cdk-lib/aws-lambda';
new lambda.DockerImageFunction(this, 'MyFn', {
functionName: 'hello',
code: lambda.DockerImageCode.fromImageAsset('./docker-fn'), // local Dockerfile
});
cdk bootstrap creates the asset ECR repository (cdk-hnb659fds-container-assets-…) via Floci's CloudFormation provisioner; cdk deploy runs docker build + docker push against the emulated registry; aws lambda invoke then pulls the image from the loopback registry and runs the handler. See compatibility-tests/compat-cdk for a working end-to-end example.
Not Implemented
The following ECR features are not implemented. Stored values for policies and lifecycle rules round-trip via the API but are not enforced at runtime:
- Replication and pull-through cache
- Image scanning (
StartImageScan,DescribeImageScanFindings) - Image signing and notary attachments
- Lifecycle policy enforcement (the policy text is stored but not applied)
- Repository policy enforcement (no IAM evaluation against repository-level policies)
- TLS via emulated ACM
Troubleshooting
Function.TimedOut when invoking image-backed Lambdas on native Linux Docker. Lambda containers reach Floci's Runtime API server via the docker bridge gateway. On Ubuntu / Pop!_OS / Debian with UFW enabled, the default INPUT DROP policy blocks this path. See Quick Start → Lambda on native Linux Docker for the one-line ufw allow in on docker0 fix.
docker login fails with TLS errors. Floci's emulated registry serves plain HTTP. Docker auto-trusts loopback addresses (127.0.0.1, *.localhost) as insecure registries, so this should not normally happen. If your URIs end up pointing somewhere non-loopback (e.g. you set FLOCI_HOSTNAME=floci for Docker Compose), add the hostname to the daemon's insecure-registries array in /etc/docker/daemon.json.
Disk not reclaimed after deleting images. BatchDeleteImage removes manifests but blobs remain on disk until garbage collection runs. Trigger it with curl -X POST http://localhost:4566/_floci/ecr/gc. The endpoint runs registry garbage-collect inside the backing container and returns the reclaimed blob list. The operation is serialized — ECR API calls block for its duration (typically a few seconds).
*.localhost does not resolve to loopback on this platform. Set floci.services.ecr.uri-style: path to fall back to localhost:<port>/<account>/<region>/<repo> URIs.