Comparing AWS Emulation Solutions for Local Development
About six months ago, my team took on a project to refactor a microservices architecture. The system relied on S3 for user image storage, SQS as a background worker queue, and DynamoDB for shopping cart sessions. Trouble soon began: each developer needed an isolated testing environment. As a result, our AWS bill shot up by over $400 that month simply because team members created test resources and forgot to delete them.
We sat down together and evaluated three potential approaches:
- Approach 1: Dedicated AWS Dev Accounts in the Cloud (Sandbox Accounts): Giving each developer a separate account or IAM user. Features and behaviors match the production environment 100%.
- Approach 2: In-code Mocking Libraries (e.g., Moto for Python, aws-sdk-mock for Node.js): Writing unit tests by intercepting SDK calls at the application layer.
- Approach 3: Running LocalStack via Docker Compose: Spinning up a LocalStack container locally that provides an AWS SDK-compatible endpoint.
Pros and Cons of Each Approach
After testing all three options across two sprints in real-world scenarios, here are the key takeaways our team noted:
1. AWS Dev Accounts in the Cloud
- Pros: Absolute accuracy. IAM permissions, network latency, and the latest AWS features match production perfectly.
- Cons: High costs. Requires a stable internet connection. Cleaning up leftover resources after each test run is tedious and time-consuming. Misconfigured policies or an accidental infinite loop between Lambda and SQS can cause your bill to skyrocket overnight.
2. In-code Mocking Libraries (Moto, aws-sdk-mock)
- Pros: Extremely fast execution. No need for external runtimes or containers.
- Cons: Only effective for unit testing. When running integration tests across multiple services—for instance, Service A pushes an event to SQS, and Service B consumes the message to store a file in S3—mocking quickly becomes convoluted and fails to validate real network flows.
3. LocalStack via Docker Compose
- Pros: Fully emulates AWS HTTP endpoints directly on your local machine. Backend code logic remains unchanged—just point
endpoint_urlto localhost. Completely free and works offline. Cleanup is as simple as running a singledocker compose downcommand. - Cons: The Community edition only supports common core services. The LocalStack container consumes around 1GB to 1.5GB of RAM while running.
Why LocalStack with Docker Compose Is the Sweet Spot
After six months of using it across eight developers and our CI/CD pipelines, this solution neatly solved both cost and convenience challenges. Newly onboarded devs simply clone the repo, run a single Docker command, and immediately have the full mock AWS infrastructure ready to test everything from web apps and APIs to background workers.
Step-by-Step Guide to Deploying LocalStack with Docker Compose
Step 1: Create the docker-compose.yml File
Create a docker-compose.yml file in your project’s root directory with the following content:
version: '3.8'
services:
localstack:
container_name: localstack-dev
image: localstack/localstack:latest
ports:
- "127.0.0.1:4566:4566" # Main gateway for all AWS services
- "127.0.0.1:4510-4559:4510-4559" # Port range for external services if needed
environment:
- DEBUG=1
- DOCKER_HOST=unix:///var/run/docker.sock
- AWS_DEFAULT_REGION=ap-southeast-1
volumes:
- "./localstack_data:/var/lib/localstack"
- "/var/run/docker.sock:/var/run/docker.sock"
Start the container in detached mode:
docker compose up -d
Step 2: Configure AWS CLI to Verify the Connection
LocalStack does not validate credentials, so you can supply any dummy values:
export AWS_ACCESS_KEY_ID=test
export AWS_SECRET_ACCESS_KEY=test
export AWS_DEFAULT_REGION=ap-southeast-1
export LOCALSTACK_URL=http://localhost:4566
Step 3: Hands-on with S3, SQS, and DynamoDB
1. Working with S3:
# Create a new bucket
aws --endpoint-url=$LOCALSTACK_URL s3 mb s3://itfromzero-bucket
# Upload a file to S3
echo "Hello from LocalStack" > test.txt
aws --endpoint-url=$LOCALSTACK_URL s3 cp test.txt s3://itfromzero-bucket/
# List files
aws --endpoint-url=$LOCALSTACK_URL s3 ls s3://itfromzero-bucket/
2. Working with SQS:
# Create an SQS queue
aws --endpoint-url=$LOCALSTACK_URL sqs create-queue --queue-name order-processing-queue
# Send a sample message to the queue
aws --endpoint-url=$LOCALSTACK_URL sqs send-message \
--queue-url http://localhost:4566/000000000000/order-processing-queue \
--message-body '{"order_id": 1024, "status": "pending"}'
# Read messages from the queue
aws --endpoint-url=$LOCALSTACK_URL sqs receive-message \
--queue-url http://localhost:4566/000000000000/order-processing-queue
3. Working with DynamoDB:
# Create the Users table
aws --endpoint-url=$LOCALSTACK_URL dynamodb create-table \
--table-name Users \
--attribute-definitions AttributeName=UserId,AttributeType=S \
--key-schema AttributeName=UserId,KeyType=HASH \
--billing-mode PAY_PER_REQUEST
# Add a record to the table
aws --endpoint-url=$LOCALSTACK_URL dynamodb put-item \
--table-name Users \
--item '{"UserId": {"S": "usr_01"}, "Name": {"S": "Nguyen Van A"}}'
# Scan all records
aws --endpoint-url=$LOCALSTACK_URL dynamodb scan --table-name Users
Quick Tip for Debugging JSON in the Terminal
When interacting with DynamoDB or SQS via CLI, the terminal often outputs raw, unformatted JSON that can be hard to read. You can use jq directly in your terminal, or paste the response into toolcraft.app/en/tools/developer/json-formatter to quickly format and inspect the data structure.
Step 4: Connecting Backend Applications (Python with Boto3 Example)
Your application simply needs to detect the development environment and pass the endpoint_url parameter when initializing AWS clients:
import os
import boto3
is_dev = os.getenv("ENV", "development") == "development"
endpoint_url = "http://localhost:4566" if is_dev else None
# Initialize S3 Client
s3_client = boto3.client(
"s3",
endpoint_url=endpoint_url,
aws_access_key_id=os.getenv("AWS_ACCESS_KEY_ID", "test"),
aws_secret_access_key=os.getenv("AWS_SECRET_ACCESS_KEY", "test"),
region_name="ap-southeast-1"
)
# List available buckets
response = s3_client.list_buckets()
print("Existing buckets:", [b['Name'] for b in response.get('Buckets', [])])
Practical Tips and Lessons Learned
- Auto-provisioning resources on startup: Mount a folder containing shell scripts to
/etc/localstack/init/ready.d/inside the container. LocalStack will automatically execute these scripts once it is ready, creating buckets and queues automatically without manual commands on everyup. - Managing volume disk usage: The
./localstack_datadirectory persists state across container restarts. However, after weeks of testing with large files, this folder can grow to several gigabytes. Consider purging it if container startup becomes sluggish. - Port 4566 is the single endpoint: Modern LocalStack versions route all services through port
4566via an Edge Router. You no longer need to track legacy individual ports like 4572 or 4576.

