Local EC2 emulator: API reference
Run EC2 instances as Docker containers locally. SSH in, run user data scripts, enforce security groups. Real containers, real networking, no AWS account needed.
Want a runnable walkthrough? See EC2: demo & walkthrough.
EC2 backend selector
# EC2_VM_MANAGER=docker is the default; this is just for clarity:
$ EC2_VM_MANAGER=docker localemu start
# Opt out of real containers for metadata-only mode:
$ EC2_VM_MANAGER=none localemu start EC2_VM_MANAGER=docker is the default; every run-instances call already launches a real Docker container. Set EC2_VM_MANAGER=none to switch to metadata-only mode (the AWS API surface still responds, but no container is created).
AMI IDs
LocalEmu ships a built-in AMI ID to Docker image map (services/ec2/docker/ami_mapping.py). Use any of these IDs with run-instances --image-id. Unknown AMI IDs fall back to localemu/ec2-base:v4; you can register a custom image by tagging it as localemu-ec2/<ami-id>.
| AMI ID | Docker image | Notes |
|---|---|---|
ami-ubuntu-22.04 | localemu/ec2-base:v4 | LocalEmu-managed Ubuntu base (v4). sshd, iptables, curl, awscli, postgresql-client, mysql-client baked in, plus an ubuntu user (uid 1000, sudo NOPASSWD) alongside root. Other AMI families get their own canonical user: ec2-user for Amazon Linux, admin for Debian, alpine for Alpine, cloud-user for RHEL. |
ami-localemu-ubuntu | localemu/ec2-base:v4 | Alias of ami-ubuntu-22.04. |
ami-ubuntu-24.04 | ubuntu:24.04 | Bare upstream image: no sshd, no LocalEmu tooling. |
ami-ubuntu-20.04 | ubuntu:20.04 | Bare upstream image. |
ami-amazon-linux-2023 (alias: ami-al2023) | amazonlinux:2023 | Bare upstream image. |
ami-amazon-linux-2 | amazonlinux:2 | Bare upstream image. |
ami-debian-12 | debian:12 | Bare upstream image. |
ami-debian-11 | debian:11 | Bare upstream image. |
ami-alpine-3.20 | alpine:3.20 | Bare upstream image. |
ami-alpine-3.18 | alpine:3.18 | Bare upstream image. |
ami-centos-9 | quay.io/centos/centos:stream9 | Bare upstream image. |
For SSH-driven workflows (the default for localemu ssh, the user-data demo, the transit-gateway tutorial) stick to ami-ubuntu-22.04: it boots with sshd already running. The bare upstream images do not include an SSH server and will not respond on port 22 without a custom user-data script.
Instance Types and Resource Limits
Each instance type maps to Docker container memory limits.
| Instance Type | Memory Limit |
|---|---|
t2.nano | 512 MB |
t2.micro | 1 GB |
t2.small | 2 GB |
t2.medium | 4 GB |
t2.large | 8 GB |
SSH Access
Create a key pair, launch an instance with it, then SSH in via the instance's PrivateIpAddress. LocalEmu does not open host-to-VPC-private-IP reachability on macOS by design, so the connection has to come from a container on the same VPC network (the "bastion pattern" -- see the EC2 & VPC networking demo for a worked example). ami-ubuntu-22.04 accepts both the ubuntu user and, for 1.1.x back-compat, root.
Create a key pair
$ awsemu ec2 create-key-pair --key-name my-key --query 'KeyMaterial' --output text > my-key.pem
$ chmod 400 my-key.pem Launch an instance
$ awsemu ec2 run-instances \
--image-id ami-ubuntu-22.04 \
--instance-type t2.micro \
--key-name my-key
InstanceId: i-abc123def456
State: pending Get its PrivateIpAddress
$ awsemu ec2 describe-instances --instance-ids i-abc123def456 \
--query 'Reservations[0].Instances[0].PrivateIpAddress' --output text
10.0.1.42 Connect via SSH
# From a container on the same VPC network (the "bastion pattern" --
# LocalEmu does not open host-to-VPC-private-IP reachability on
# macOS by design):
$ ssh -i my-key.pem ubuntu@10.0.1.42
# root still works too, for 1.1.x back-compat:
$ ssh -i my-key.pem root@10.0.1.42 EC2 Instance Connect
SendSSHPublicKey pushes an ephemeral public key into a marker-wrapped block in the target user's authorized_keys, matching real AWS's 60-second TTL: the key works immediately and is automatically removed a minute later. SendSerialConsoleSSHPublicKey returns the AWS-shaped SerialConsoleAccessDisabledException (no serial console emulation).
$ ssh-keygen -t ed25519 -f /tmp/eic-key -N ""
$ aws ec2-instance-connect send-ssh-public-key \
--instance-id i-abc123def456 \
--instance-os-user ubuntu \
--ssh-public-key file:///tmp/eic-key.pub
{"RequestId": "...", "Success": true}
# valid for 60 seconds, from a container on the same VPC network:
$ ssh -i /tmp/eic-key ubuntu@10.0.1.42 localemu ssh Command
The localemu ssh command is a docker exec shortcut into the instance's container, run from the host. It does not use SSH, key pairs, or the VPC network, so it works even without a bastion container.
$ localemu ssh i-abc123def456
# docker exec -it into the instance container, no SSH involved
$ localemu ssh --list
NAMES STATUS PORTS
localemu-ec2-i-abc123def456 Up 2 minutes SSM Session Manager
Start a real interactive shell using the actual aws ssm start-session CLI, no wrapper and no key pair required. The wire codec matches the real session-manager-plugin byte-for-byte, so the standard AWS tooling works directly against LocalEmu.
$ aws ssm start-session --target i-abc123def456
Starting session with SessionId: ...
root@i-abc123def456:/# AWS-StartPortForwardingSession is supported too: LocalEmu speaks the same xtaci/smux v1 framing the real session-manager-plugin uses to multiplex a local TCP port over the SSM WebSocket channel. Point any TCP client at the forwarded local port and it reaches the target port inside the instance.
$ aws ssm start-session --target i-abc123def456 \
--document-name AWS-StartPortForwardingSession \
--parameters 'portNumber=8012,localPortNumber=18012'
Starting session with SessionId: ...
Port 18012 opened for sessionId ...
Waiting for connections...
# from another terminal:
$ curl http://127.0.0.1:18012/ User Data
Pass a startup script with --user-data. The script runs automatically when the container boots.
$ awsemu ec2 run-instances \
--image-id ami-ubuntu-22.04 \
--instance-type t2.small \
--key-name my-key \
--user-data file://setup.sh Instance Metadata Service (IMDS)
A per-VPC IMDS sidecar serves the full /latest/meta-data/ tree (instance-id, ami-id, instance-type, local hostname + IP, IAM credentials, ...) and is reachable from inside the container two ways: an iptables OUTPUT-chain DNAT rule rewrites every packet to the link-local 169.254.169.254:80 address that boto3 / awscli / the AWS SDKs hardcode, and the AWS_EC2_METADATA_SERVICE_ENDPOINT env var is also injected for the small set of clients that honour it. Both paths land on the same sidecar.
Security Groups
Security group rules are enforced via a TCP proxy. Rules are evaluated at connection time. Changes from AuthorizeSecurityGroupIngress take effect immediately.
$ awsemu ec2 create-security-group \
--group-name web-sg \
--description "Allow HTTP"
GroupId: sg-abc123
$ awsemu ec2 authorize-security-group-ingress \
--group-id sg-abc123 \
--protocol tcp \
--port 80 \
--cidr 0.0.0.0/0 SourceDestCheck (NAT / router instances)
Real AWS blocks an instance from forwarding traffic not addressed to itself unless SourceDestCheck is disabled on the relevant ENI -- the standard way to run a NAT instance or a router. LocalEmu enforces this with a rule per attached ENI in the container's iptables FORWARD chain, keyed on the interface name when each ENI has its own interface, or on source/destination IP when two ENIs share the same eth1 (two AWS-side ENIs landing on the same VPC bridge). The container's policy stays -P FORWARD DROP by default; per-ENI ACCEPT rules selectively open forwarding. The kernel's net.ipv4.ip_forward bit is set as soon as any ENI has it disabled, and cleared again once none do.
# Turn off SourceDestCheck on a router/NAT instance's primary ENI
$ awsemu ec2 modify-instance-attribute \
--instance-id i-abc12345 \
--source-dest-check '{"Value": false}'
# Or target one specific ENI directly (multi-NIC instances)
$ awsemu ec2 modify-network-interface-attribute \
--network-interface-id eni-67890 \
--no-source-dest-check ModifyNetworkInterfaceAttribute targets the specific ENI in the call. ModifyInstanceAttribute(SourceDestCheck=...) mirrors onto the primary ENI, matching real AWS, and then applies the per-ENI rule on the primary's interface -- so DescribeInstances and DescribeNetworkInterfaces agree, and the forwarding behavior actually changes rather than just the reported flag.
Known limitation: when a primary and a secondary ENI share eth1 and the primary has SourceDestCheck=false, the primary's rule allows traffic on eth1 regardless of which of the two ENIs it conceptually belongs to. Single-NIC instances and multi-VPC setups where each ENI gets its own interface are unaffected.
Container Naming
Each EC2 instance runs in a Docker container named localemu-ec2-<instance-id>.
$ docker ps --filter "label=localemu.service=ec2"
CONTAINER ID IMAGE STATUS NAMES
a1b2c3d4e5f6 localemu/ec2-base:v4 Up 2 min localemu-ec2-i-abc123def456 Instance Lifecycle
EC2 instances follow the standard lifecycle. Each state maps to a Docker container state.