ECR
Protocol: JSON 1.1 (X-Amz-Target: AmazonEC2ContainerRegistry_V20150921.*) for the control plane.
Data plane: OCI Distribution Spec v2 (/v2/...), served by a real registry:2 container managed by Floci.
Endpoint: POST http://localhost:4566/ for the control plane; <account>.dkr.ecr.<region>.localhost:<port>/<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 (round-trip; not enforced on push) |
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 serves all repositories. The container is started lazily on the first ECR API call and reused across Floci restarts (keep-running-on-shutdown: trueby default), so pushed image bytes survive restarts. - Loopback URI scheme. Repository URIs follow
<account>.dkr.ecr.<region>.localhost:<registryPort>/<repoName>. RFC 6761 reserves*.localhostto resolve to the loopback address, and the docker daemon auto-trusts loopback as an insecure registry, so no daemon configuration changes are required —docker pushanddocker pullwork out of the box. ApathURI style fallback (localhost:<port>/<account>/<region>/<repo>) is available viafloci.services.ecr.uri-style: pathfor environments where*.localhostresolution misbehaves. - 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. - Lambda integration. Image-backed Lambda functions (
PackageType=Image) reference the same loopbackrepositoryUri. Floci's Lambda runner 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.
Configuration
Defaults under floci.services.ecr in application.yml:
floci:
services:
ecr:
enabled: true
registry-image: "registry:2"
registry-container-name: floci-ecr-registry
registry-base-port: 5100
registry-max-port: 5199
data-path: ./data/ecr
tls-enabled: false
keep-running-on-shutdown: true
uri-style: hostname # or "path"
| Setting | Default | Description |
|---|---|---|
enabled |
true |
Enable the ECR control plane and lazy registry start |
registry-image |
registry:2 |
Backing OCI registry image |
registry-container-name |
floci-ecr-registry |
Name used for idempotent reuse across restarts |
registry-base-port / -max-port |
5100 / 5199 |
Port range allocated for the published registry port |
data-path |
./data/ecr |
Bind-mount root for <data-path>/registry (the registry's /var/lib/registry) |
keep-running-on-shutdown |
true |
Leave the container up so the next Floci start adopts it |
uri-style |
hostname |
hostname returns *.dkr.ecr.<region>.localhost; path returns localhost:<port>/<account>/<region>/<repo> |
tls-enabled |
false |
Reserved for the future ACM-backed TLS phase |
Docker Compose port mapping
The ECR registry sidecar container binds its host port directly — do not add 5100-5199 to the floci service's ports in docker-compose.yml. Adding that range pre-allocates those ports on the floci container and prevents the sidecar from binding them:
# Correct — no ECR port range on the floci service
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 localhost:5100 works automatically once Floci starts the registry sidecar — no additional port mapping is needed.
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:5100/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:5100
# Push an image
docker pull alpine:3.19
docker tag alpine:3.19 \
000000000000.dkr.ecr.us-east-1.localhost:5100/floci-it/app:v1
docker push 000000000000.dkr.ecr.us-east-1.localhost:5100/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:5100/floci-it/app:v1
docker pull 000000000000.dkr.ecr.us-east-1.localhost:5100/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:5100/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
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.