ECS Docker Containers
Run real Docker containers from ECS task definitions. Create clusters, register task definitions, run tasks, and see actual containers running on your machine.
nginx:alpine web tier with a port mapping,
plus a small Python worker that loops on stdout),
invokes RunTask for both, waits for
describe-tasks to report
RUNNING, resolves the host port the nginx
container was published on via docker port,
curls the welcome page back, tails the
worker's stdout, then stops both tasks by their full ARN (the one
run-task returned),
deregisters the task definitions, and deletes the cluster.
Docker backend is on by default when Docker is reachable; set
ECS_DOCKER_BACKEND=0 to fall back to the
metadata-only Moto path.
Source: 16-ecs-demo/ in the examples repo.
Containers on your host
Each container in a task definition runs as a Docker container managed by the LocalEmu ECS Docker backend, labeled localemu.service=ecs.
Port Mappings
Container ports are mapped to random host ports. Access your services via localhost.
Full Lifecycle
Create clusters, register tasks, run, describe, stop, and delete. Complete ECS workflow.
Step-by-Step Walkthrough
Step 1: Start LocalEmu
$ localemu start
# Docker backend is on by default. Set ECS_DOCKER_BACKEND=0 to disable. The Docker backend turns on automatically when LocalEmu can reach a Docker socket. Override with ECS_DOCKER_BACKEND=0 to run ECS metadata-only (no real containers), useful for ARN/lifecycle smoke tests that do not need to actually pull images.
Step 2: Create a cluster
$ awsemu ecs create-cluster --cluster-name my-app
cluster:
clusterArn: arn:aws:ecs:us-east-1:000000000000:cluster/my-app
clusterName: my-app
status: ACTIVE
registeredContainerInstancesCount: 0
runningTasksCount: 0
pendingTasksCount: 0
activeServicesCount: 0 The cluster is created with ACTIVE status. All task state is tracked under this cluster name.
Step 3: Register task definitions
$ awsemu ecs register-task-definition \
--family le-web \
--container-definitions '[{
"name": "nginx",
"image": "nginx:alpine",
"portMappings": [{"containerPort": 80, "hostPort": 0}],
"essential": true,
"memory": 128
}]'
taskDefinition:
taskDefinitionArn: arn:aws:ecs:us-east-1:000000000000:task-definition/le-web:1
family: le-web
status: ACTIVE
$ awsemu ecs register-task-definition \
--family le-worker \
--container-definitions '[{
"name": "worker",
"image": "python:3.12-slim",
"command": ["python", "-c", "import time; [print(f\"tick {i}\", flush=True) or time.sleep(1) for i in range(3600)]"],
"essential": true,
"memory": 256
}]'
taskDefinition:
taskDefinitionArn: arn:aws:ecs:us-east-1:000000000000:task-definition/le-worker:1
family: le-worker
status: ACTIVE
$ awsemu ecs list-task-definitions
taskDefinitionArns:
- arn:aws:ecs:us-east-1:000000000000:task-definition/le-web:1
- arn:aws:ecs:us-east-1:000000000000:task-definition/le-worker:1 Two task definitions registered: le-web runs nginx with port 80 exposed, and le-worker runs a Python script. Both use real Docker images.
Step 4: Run tasks
$ awsemu ecs run-task --cluster my-app --task-definition le-web
tasks:
- taskArn: arn:aws:ecs:us-east-1:000000000000:task/my-app/a1b2c3d4e5f6
clusterArn: arn:aws:ecs:us-east-1:000000000000:cluster/my-app
taskDefinitionArn: arn:aws:ecs:us-east-1:000000000000:task-definition/le-web:1
lastStatus: RUNNING
desiredStatus: RUNNING
containers:
- name: nginx
lastStatus: RUNNING
networkBindings:
- containerPort: 80
hostPort: 56816
protocol: tcp
$ awsemu ecs run-task --cluster my-app --task-definition le-worker
tasks:
- taskArn: arn:aws:ecs:us-east-1:000000000000:task/my-app/f7e8d9c0b1a2
clusterArn: arn:aws:ecs:us-east-1:000000000000:cluster/my-app
taskDefinitionArn: arn:aws:ecs:us-east-1:000000000000:task-definition/le-worker:1
lastStatus: RUNNING
desiredStatus: RUNNING
containers:
- name: worker
lastStatus: RUNNING Each task starts real Docker containers. The nginx container gets host port 56816 mapped to container port 80. Both tasks show RUNNING status immediately.
Step 5: Verify containers on the host
$ docker ps --filter "label=localemu.service=ecs"
CONTAINER ID IMAGE COMMAND STATUS PORTS NAMES
3f8a1b2c4d5e nginx:alpine "/docker-entrypoint.…" Up 12 seconds 0.0.0.0:56816->80/tcp localemu-ecs-a1b2c3d4e5f6-nginx
7e6d5c4b3a2f python:3.12-slim "python -c import t…" Up 8 seconds localemu-ecs-f7e8d9c0b1a2-worker
$ curl -s http://localhost:56816 | head -4
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title> The localemu.service=ecs label is set on every container the ECS Docker backend starts, so a single docker ps --filter covers every cluster, every task, every container. The nginx HTTP response is the upstream nginx:alpine welcome page, served on the host port LocalEmu chose at RunTask time and reported back in networkBindings[].hostPort.
Step 6: Describe and monitor
$ awsemu ecs describe-tasks --cluster my-app \
--tasks a1b2c3d4e5f6 f7e8d9c0b1a2
tasks:
- taskArn: arn:aws:ecs:us-east-1:000000000000:task/my-app/a1b2c3d4e5f6
lastStatus: RUNNING
taskDefinitionArn: arn:aws:ecs:us-east-1:000000000000:task-definition/le-web:1
containers:
- name: nginx
lastStatus: RUNNING
- taskArn: arn:aws:ecs:us-east-1:000000000000:task/my-app/f7e8d9c0b1a2
lastStatus: RUNNING
taskDefinitionArn: arn:aws:ecs:us-east-1:000000000000:task-definition/le-worker:1
containers:
- name: worker
lastStatus: RUNNING
$ awsemu ecs list-tasks --cluster my-app
taskArns:
- arn:aws:ecs:us-east-1:000000000000:task/my-app/a1b2c3d4e5f6
- arn:aws:ecs:us-east-1:000000000000:task/my-app/f7e8d9c0b1a2 DescribeTasks shows real container status. ListTasks returns all task ARNs in the cluster. Same API responses as real AWS.
Step 7: Stop and cleanup
$ awsemu ecs stop-task --cluster my-app --task a1b2c3d4e5f6
task:
taskArn: arn:aws:ecs:us-east-1:000000000000:task/my-app/a1b2c3d4e5f6
lastStatus: STOPPED
desiredStatus: STOPPED
$ awsemu ecs stop-task --cluster my-app --task f7e8d9c0b1a2
task:
taskArn: arn:aws:ecs:us-east-1:000000000000:task/my-app/f7e8d9c0b1a2
lastStatus: STOPPED
desiredStatus: STOPPED
$ docker ps --filter "label=localemu.service=ecs"
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
(empty - containers stopped)
$ awsemu ecs delete-cluster --cluster-name my-app
cluster:
clusterArn: arn:aws:ecs:us-east-1:000000000000:cluster/my-app
clusterName: my-app
status: INACTIVE StopTask kills the Docker containers. DeleteCluster marks the cluster as INACTIVE. No orphaned containers, no leftover resources.
How It Works
Metadata layer
Cluster records, task definitions, and task state are stored in the in-process registry so every Describe/List/Run call returns the same shape AWS returns.
Behavior layer (Docker)
Every container in a task definition runs as a real Docker container. Port mappings, env vars, commands, and exit codes come straight from the runtime, not a stub. This is what actually serves traffic.
Full lifecycle
RunTask starts containers, DescribeTasks shows real status, StopTask kills containers, DeleteCluster cleans everything up.
Supported Operations
- ●CreateCluster, DeleteCluster
- ●RegisterTaskDefinition, ListTaskDefinitions, DescribeTaskDefinition
- ●RunTask with real Docker containers
- ●StopTask with real container shutdown
- ●DescribeTasks with real container status
- ●ListTasks
- ●Port mappings (random host port assignment)
- ●Environment variables from task definition
- ●Custom commands per container
- ●Container labels for identification