EC2: demo & walkthrough
Launch real Docker containers as EC2 instances. SSH in, run user data scripts, manage instance lifecycle. No AWS account needed, no VMs, just containers that behave like EC2 instances.
Looking for the API surface and CLI examples? See EC2: API reference.
t2.micro
Ubuntu 22.04 instance with a user-data script that writes
/tmp/hello.txt at boot, waits for the
instance to reach running, SSHes in with
the key pair to read the user-data output back, then exercises the lifecycle:
stop-instances ->
start-instances ->
terminate-instances, verifying the Docker
container state at each step. Instances boot from the pre-baked
localemu/ec2-base image with
openssh-server already installed, so
sshd is reachable in ~1 second.
Requires EC2_VM_MANAGER=docker on the
LocalEmu host.
Source: 15-ec2-demo/ in the examples repo.
Real SSH Access
SSH into instances with real key pairs. Use standard ssh or the localemu ssh shortcut.
SSM Sessions
Interactive shell via awsemu ssm start-session. Drop into a root shell instantly.
User Data Scripts
Bootstrap scripts run at boot. Install packages, write files, configure services.
Security Groups
Create and attach security groups with ingress rules. Full API compatibility.
IMDS Metadata
Instance Metadata Service available inside containers. Query instance identity like real EC2.
Step-by-Step Walkthrough
Step 1: Start LocalEmu with EC2 Docker backend
EC2_VM_MANAGER=docker localemu start The EC2_VM_MANAGER=docker flag tells LocalEmu to create real Docker containers when you launch EC2 instances.
Step 2: Create an SSH key pair
$ awsemu ec2 create-key-pair --key-name my-key \
--query KeyMaterial --output text > /tmp/my-key.pem
$ chmod 400 /tmp/my-key.pem LocalEmu generates a real RSA key pair. The private key is saved to a file, and the public key is injected into instances that reference this key name.
Step 3: Create a security group with SSH access
$ SG_ID=$(awsemu ec2 create-security-group \
--group-name ssh-open \
--description "SSH open" \
--query 'GroupId' --output text)
$ echo $SG_ID
sg-df9813973ad922ec6
$ awsemu ec2 authorize-security-group-ingress \
--group-id $SG_ID --protocol tcp --port 22 --cidr 0.0.0.0/0
Return: true
SecurityGroupRules:
- SecurityGroupRuleId: sgr-ee7274d05c857c4c3
GroupId: sg-df9813973ad922ec6
IpProtocol: tcp
FromPort: 22
ToPort: 22
CidrIpv4: 0.0.0.0/0 Security groups are created through the standard EC2 API. Ingress rules control which ports are accessible.
Step 4: Launch an instance with key pair, security group, and user data
$ awsemu ec2 run-instances \
--image-id ami-ubuntu-22.04 \
--instance-type t2.micro \
--count 1 \
--key-name my-key \
--security-group-ids $SG_ID \
--user-data '#!/bin/bash
echo "Hello from LocalEmu EC2" > /tmp/hello.txt
date > /tmp/boot-time.txt'
InstanceId: i-345e9b5f68e616e47
ImageId: ami-ubuntu-22.04
InstanceType: t2.micro
State: pending
KeyName: my-key
SecurityGroups:
- GroupId: sg-df9813973ad922ec6
GroupName: ssh-open
Tags:
- Key: localemu:ssh-port
Value: "50025"
PublicIpAddress: 127.0.0.1
PrivateIpAddress: 10.43.9.71 LocalEmu boots the localemu/ec2-base:v3 pre-baked image (Ubuntu + openssh-server + awscli + tooling), starts a container, injects the SSH public key, and runs the user data script. The localemu:ssh-port tag tells you which host port maps to SSH.
Step 5: Describe the running instance
$ awsemu ec2 describe-instances --instance-ids i-345e9b5f68e616e47
InstanceId: i-345e9b5f68e616e47
State: running
PublicDnsName: localhost
PublicIpAddress: 127.0.0.1
Tags:
- Key: localemu:ssh-port
Value: "50025"
KeyName: my-key
InstanceType: t2.micro
ImageId: ami-ubuntu-22.04
AvailabilityZone: us-east-1a Once running, PublicDnsName is set to localhost and PublicIpAddress to 127.0.0.1. The SSH port is in the instance tags.
Step 6: Verify the Docker container is running
$ docker ps --filter "label=localemu.service=ec2"
CONTAINER ID IMAGE COMMAND STATUS PORTS NAMES
ee1fb33a5530 localemu/ec2-base:v3 "sh -c '#!/bin/sh\nse..." Up 45 seconds 0.0.0.0:50021->22/tcp, [::]:50021->22/tcp localemu-ec2-i-345e9b5f68e616e47 Step 7: List SSH-accessible instances
$ localemu ssh --list
NAMES STATUS PORTS
localemu-ec2-i-345e9b5f68e616e47 Up About a minute 0.0.0.0:50021->22/tcp, [::]:50021->22/tcp Step 8: Verify user data ran successfully
$ localemu ssh i-345e9b5f68e616e47 cat /tmp/hello.txt
Hello from LocalEmu EC2 The user data script wrote "Hello from LocalEmu EC2" to /tmp/hello.txt at boot time. The localemu ssh command connects to the instance by ID without needing to know the port.
Step 9: Check the OS inside the instance
$ localemu ssh i-345e9b5f68e616e47 cat /etc/os-release
PRETTY_NAME="Ubuntu 22.04.5 LTS"
NAME="Ubuntu"
VERSION_ID="22.04"
VERSION="22.04.5 LTS (Jammy Jellyfish)"
VERSION_CODENAME=jammy
ID=ubuntu
ID_LIKE=debian Ubuntu 22.04.5 LTS running inside the container. The OS image is the upstream ubuntu:22.04 with openssh-server, iptables, awscli and basic net tooling baked in by localemu/ec2-base.
Step 10: Start an interactive SSM session
$ awsemu ssm start-session --target i-345e9b5f68e616e47
root@ee1fb33a5530:/# ls
bin boot dev etc home lib media mnt opt proc root run sbin srv sys tmp usr var
root@ee1fb33a5530:/# exit
exit SSM start-session drops you into a root shell inside the container. Full interactive access, just like real AWS SSM.
Step 11: Connect with standard SSH using your key pair
$ SSH_PORT=$(awsemu ec2 describe-instances \
--instance-ids i-345e9b5f68e616e47 \
--query 'Reservations[0].Instances[0].Tags[?Key==`localemu:ssh-port`].Value' \
--output text)
$ echo "SSH port: $SSH_PORT"
SSH port: 50025
$ ssh -i /tmp/my-key.pem -p $SSH_PORT \
-o StrictHostKeyChecking=no root@localhost whoami
root Standard OpenSSH works. The SSH port is available in the instance tags. Use localhost as the host and the mapped port.
Step 12: Stop and restart the instance
$ awsemu ec2 stop-instances --instance-ids i-345e9b5f68e616e47
InstanceId: i-345e9b5f68e616e47
CurrentState: stopping
PreviousState: running
$ docker ps --filter "label=localemu.service=ec2"
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
(empty - container stopped)
$ awsemu ec2 start-instances --instance-ids i-345e9b5f68e616e47
InstanceId: i-345e9b5f68e616e47
CurrentState: pending
PreviousState: stopped
$ docker ps --filter "label=localemu.service=ec2"
CONTAINER ID IMAGE COMMAND STATUS PORTS NAMES
ee1fb33a5530 localemu/ec2-base:v3 "sh -c '#!/bin/sh\nse..." Up 13 seconds 0.0.0.0:50021->22/tcp, [::]:50021->22/tcp localemu-ec2-i-345e9b5f68e616e47 Stopping an instance stops the Docker container. Starting it again brings the same container back. Full lifecycle management.
Step 13: Terminate and cleanup
$ awsemu ec2 terminate-instances --instance-ids i-345e9b5f68e616e47
InstanceId: i-345e9b5f68e616e47
CurrentState: shutting-down
PreviousState: running
$ docker ps --filter "label=localemu.service=ec2"
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
(empty - container removed)
$ localemu stop
LocalEmu stopped. Terminating removes the Docker container completely. No orphaned containers, no leftover resources.
Python Integration
Use boto3 to drive the same demo programmatically. The script reads AWS_ENDPOINT_URL from the environment; unset it (or point at real AWS) and the identical code runs against the real EC2 API. Credentials come from the standard boto3 chain, no hardcoded secrets in source.
import os
import boto3
# Point boto3 at LocalEmu via the standard AWS_ENDPOINT_URL env var,
# or set endpoint_url here directly. Credentials come from the usual
# boto3 resolution chain (env vars, shared config, instance profile, etc).
ec2 = boto3.client(
"ec2",
endpoint_url=os.environ.get("AWS_ENDPOINT_URL", "http://localhost:4566"),
region_name="us-east-1",
)
# Create key pair
key = ec2.create_key_pair(KeyName="my-key")
with open("/tmp/my-key.pem", "w") as f:
f.write(key["KeyMaterial"])
# Create security group with SSH access
sg = ec2.create_security_group(
GroupName="ssh-access",
Description="Allow SSH",
)
ec2.authorize_security_group_ingress(
GroupId=sg["GroupId"],
IpProtocol="tcp",
FromPort=22,
ToPort=22,
CidrIp="0.0.0.0/0",
)
# Launch instance with user data
user_data = '''#!/bin/bash
echo "Hello from LocalEmu EC2" > /tmp/hello.txt'''
response = ec2.run_instances(
ImageId="ami-ubuntu-22.04",
InstanceType="t2.micro",
MinCount=1, MaxCount=1,
KeyName="my-key",
SecurityGroupIds=[sg["GroupId"]],
UserData=user_data,
)
instance_id = response["Instances"][0]["InstanceId"]
print(f"Launched: {instance_id}")
# Wait for running state
ec2.get_waiter("instance_running").wait(InstanceIds=[instance_id])
# Read the SSH host port LocalEmu mapped to this instance
desc = ec2.describe_instances(InstanceIds=[instance_id])
tags = desc["Reservations"][0]["Instances"][0].get("Tags", [])
ssh_port = next(t["Value"] for t in tags if t["Key"] == "localemu:ssh-port")
print(f"SSH available on localhost:{ssh_port}")
# Terminate when done
ec2.terminate_instances(InstanceIds=[instance_id])
print(f"Terminated: {instance_id}") Supported AMI Images
| AMI ID | Docker Image | Description |
|---|---|---|
| (default for every AMI ID) | localemu/ec2-base:v3 | Pre-baked Ubuntu base with openssh-server, iptables, curl, awscli, and other net tooling. Built once on first RunInstances call. |
How It Works
DockerVmManager picks up the new instance, ensures the localemu/ec2-base image exists (built once on first run), and starts a container on the right Docker network for the target VPC. /root/.ssh/authorized_keys inside the container, so the private PEM on your host actually opens the door. /var/lib/cloud/instance/scripts/ and executed at boot before sshd is reachable. localemu:ssh-port tag (and consumed by localemu ssh). http://169.254.169.254/ from inside the container via an iptables DNAT rule, so tooling that hardcodes the link-local IMDS address (boto3, awscli, the AWS SDKs) finds credentials and instance identity exactly where it expects. stop-instances stops the container, start-instances brings the same container back, terminate-instances removes it. The EC2 metadata record stays in sync with the container's lifecycle.