Docs / EC2 Docker Instances

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

Terminal
# 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

Terminal
$ awsemu ec2 create-key-pair --key-name my-key --query 'KeyMaterial' --output text > my-key.pem
$ chmod 400 my-key.pem

Launch an instance

Terminal
$ 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

Terminal
$ awsemu ec2 describe-instances --instance-ids i-abc123def456 \
    --query 'Reservations[0].Instances[0].PrivateIpAddress' --output text

10.0.1.42

Connect via SSH

Terminal
# 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).

Terminal
$ 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.

Terminal
$ 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.

Terminal
$ 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.

Terminal
$ 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.

Terminal
$ 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.

Terminal
$ 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.

Terminal
# 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>.

Terminal
$ 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.

pending running stopping stopped terminated