Module 4: LocalStack — AWS Local Development
2. LocalStack Setup and Configuration
Overview
In this capsule you'll install and configure LocalStack as a service inside your Docker Compose — the same Compose you built in Module 2 with FastAPI and Redis. By the end, you'll have LocalStack running alongside your AI app, ready to emulate S3, Lambda, and other AWS services. It's not an isolated installation: LocalStack integrates into your existing development infrastructure.
Context: The previous capsule explained what LocalStack is and why it matters. Here you get it up and running. The setup is deliberately simple: 5 lines in your Compose file and one command to verify. The value isn't in installing it — it's in what you do with it in the following capsules.
LocalStack in Docker Compose
The minimal configuration
LocalStack runs as a Docker container. The cleanest way to integrate it is as one more service in your Docker Compose:
# docker-compose.yml — Add LocalStack to the M2 Compose
services:
# --- Existing services from M2 ---
api:
build:
context: ./api
ports:
- "8000:8000"
env_file:
- .env
environment:
- REDIS_URL=redis://cache:6379
- AWS_ENDPOINT_URL=http://localstack:4566
depends_on:
cache:
condition: service_healthy
localstack:
condition: service_healthy
cache:
image: redis:7-alpine
command: redis-server --appendonly yes --maxmemory 128mb --maxmemory-policy allkeys-lru
volumes:
- redis_data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 3
# --- NEW: LocalStack ---
localstack:
image: localstack/localstack:latest
ports:
- "4566:4566"
environment:
- SERVICES=s3,lambda
- DEBUG=0
- LAMBDA_EXECUTOR=docker
- DOCKER_HOST=unix:///var/run/docker.sock
volumes:
- localstack_data:/var/lib/localstack
- /var/run/docker.sock:/var/run/docker.sock
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:4566/_localstack/health"]
interval: 10s
timeout: 5s
retries: 5
start_period: 15s
volumes:
redis_data:
localstack_data:
Each line explained
localstack:
image: localstack/localstack:latest
# Official LocalStack image from Docker Hub
ports:
- "4566:4566"
# A single port for ALL AWS services
# S3, Lambda, API Gateway — everything through port 4566
# LocalStack routes internally based on the requested service
environment:
- SERVICES=s3,lambda
# Only start the services you need
# Fewer services = faster startup, less RAM
# For this guide: S3 and Lambda are enough
- DEBUG=0
# 0 = normal logs, 1 = verbose logs
# Use 1 when you're debugging problems
- LAMBDA_EXECUTOR=docker
# Lambda runs functions in separate Docker containers
# Alternative: "local" (runs in the same process)
# "docker" is more faithful to real AWS
- DOCKER_HOST=unix:///var/run/docker.sock
# Lets LocalStack create containers for Lambda
# Required when LAMBDA_EXECUTOR=docker
volumes:
- localstack_data:/var/lib/localstack
# Persists data between container restarts
# Without this, you lose buckets and functions on restart
- /var/run/docker.sock:/var/run/docker.sock
# Gives access to the Docker socket to create Lambda containers
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:4566/_localstack/health"]
# Verifies that LocalStack is ready
# Services aren't available until the health check passes
interval: 10s
timeout: 5s
retries: 5
start_period: 15s
# start_period: LocalStack takes ~10-15s to start
Why a single port
Unlike real AWS (where S3 is at s3.amazonaws.com, Lambda at lambda.us-east-1.amazonaws.com), LocalStack exposes everything through a single port: 4566. The service is determined by the request headers, not by the URL.
Real AWS:
├── s3.amazonaws.com → S3
├── lambda.us-east-1.amazonaws.com → Lambda
└── apigateway.us-east-1.amazonaws.com → API Gateway
LocalStack:
└── localhost:4566 → Everything
├── boto3 sends a header with the requested service
└── LocalStack routes internally
This simplifies your setup: one port, one container, all the services.
CLI Configuration
awslocal: your new best friend
awslocal is a wrapper for the AWS CLI that automatically adds --endpoint-url=http://localhost:4566:
# Install
pip install awscli-local
# Verify
awslocal --version
Comparison:
# Without awslocal (verbose, easy to forget the flag)
aws --endpoint-url=http://localhost:4566 s3 ls
aws --endpoint-url=http://localhost:4566 lambda list-functions
aws --endpoint-url=http://localhost:4566 s3 mb s3://my-bucket
# With awslocal (clean, same result)
awslocal s3 ls
awslocal lambda list-functions
awslocal s3 mb s3://my-bucket
Configure dummy credentials
LocalStack doesn't validate credentials (Community Edition), but the AWS CLI requires them to exist. Configure dummy credentials:
# Configure fake credentials (LocalStack ignores them)
aws configure set aws_access_key_id test
aws configure set aws_secret_access_key test
aws configure set region us-east-1
# Verify
aws configure list
# Should show access_key and secret_key as "test"
These credentials never reach real AWS — they just satisfy the CLI's requirement. LocalStack accepts any value.
Alternative environment variables
If you'd rather not modify your ~/.aws/credentials, use environment variables:
# In your .env or in your shell
export AWS_ACCESS_KEY_ID=test
export AWS_SECRET_ACCESS_KEY=test
export AWS_DEFAULT_REGION=us-east-1
export AWS_ENDPOINT_URL=http://localhost:4566
With AWS_ENDPOINT_URL configured, even the normal aws CLI points to LocalStack without needing awslocal:
# With AWS_ENDPOINT_URL configured:
aws s3 ls # ← this already points to LocalStack
Start and Verify
First startup
# Start everything (from your project directory)
docker compose up -d
# Verify that all services are up
docker compose ps
# Expected output:
# NAME STATUS PORTS
# api Up (healthy) 0.0.0.0:8000->8000/tcp
# cache Up (healthy) 6379/tcp
# localstack Up (healthy) 0.0.0.0:4566->4566/tcp
Verify available services
# LocalStack health check
curl http://localhost:4566/_localstack/health | python3 -m json.tool
# Expected output:
# {
# "services": {
# "s3": "available",
# "lambda": "available"
# },
# "version": "3.x.x"
# }
Verify with awslocal
# S3: list buckets (empty at the start)
awslocal s3 ls
# (no output — no buckets yet)
# Lambda: list functions (empty at the start)
awslocal lambda list-functions
# {"Functions": []}
# Create a test bucket
awslocal s3 mb s3://test-bucket
# make_bucket: test-bucket
# Verify it exists
awslocal s3 ls
# 2026-03-08 12:00:00 test-bucket
# Clean up
awslocal s3 rb s3://test-bucket
Verify with Python (boto3)
# test_localstack.py
import boto3
s3 = boto3.client(
"s3",
endpoint_url="http://localhost:4566",
aws_access_key_id="test",
aws_secret_access_key="test",
region_name="us-east-1",
)
# Create bucket
s3.create_bucket(Bucket="test-from-python")
# List buckets
response = s3.list_buckets()
for bucket in response["Buckets"]:
print(f"Bucket: {bucket['Name']}")
# Expected output:
# Bucket: test-from-python
# Clean up
s3.delete_bucket(Bucket="test-from-python")
print("LocalStack + boto3 working correctly")
python test_localstack.py
# Bucket: test-from-python
# LocalStack + boto3 working correctly
If you see that output, your setup is complete.
Advanced Configurations
SERVICES: control what starts
# Only S3 and Lambda (what we need)
environment:
- SERVICES=s3,lambda
# Everything (slower to start, more RAM)
environment:
- SERVICES= # empty = all
# S3, Lambda, SQS, DynamoDB
environment:
- SERVICES=s3,lambda,sqs,dynamodb
Rule: only list the services you use. Each additional service consumes RAM and slows down the startup.
LAMBDA_EXECUTOR: how it runs functions
# docker (recommended) — each function in its container
environment:
- LAMBDA_EXECUTOR=docker
# local — runs in the LocalStack process (faster, less faithful)
environment:
- LAMBDA_EXECUTOR=local
docker is more faithful to real AWS but requires access to the Docker socket. local is faster for iteration but can have behavioral differences.
Persistence with volumes
# Without a volume: data is lost on restart
localstack:
image: localstack/localstack:latest
# With a volume: data persists
localstack:
image: localstack/localstack:latest
volumes:
- localstack_data:/var/lib/localstack
In the Community Edition, persistence is limited. To guarantee your environment is ready, use an initialization script:
#!/bin/bash
# scripts/setup-localstack.sh
echo "Waiting for LocalStack to be ready..."
until curl -s http://localhost:4566/_localstack/health | grep -q '"s3": "available"'; do
sleep 2
done
echo "LocalStack ready"
echo "Creating buckets..."
awslocal s3 mb s3://ai-input
awslocal s3 mb s3://ai-output
echo "Verifying..."
awslocal s3 ls
echo "Setup complete"
Init hooks: automatic setup on startup
LocalStack supports initialization scripts that run on startup:
localstack:
image: localstack/localstack:latest
volumes:
- ./init-scripts:/etc/localstack/init/ready.d
# init-scripts/setup.sh
#!/bin/bash
awslocal s3 mb s3://ai-input
awslocal s3 mb s3://ai-output
echo "Buckets created automatically"
Any script in /etc/localstack/init/ready.d/ runs when LocalStack is ready. This eliminates the need to run the setup manually.
LocalStack Inside the Docker Network
Communication between services
Inside Docker Compose, your FastAPI app can access LocalStack using the service name as the hostname:
# From your FastAPI (inside the Compose)
s3 = boto3.client(
"s3",
endpoint_url="http://localstack:4566", # the service's hostname
aws_access_key_id="test",
aws_secret_access_key="test",
)
# From your machine (outside the Compose)
s3 = boto3.client(
"s3",
endpoint_url="http://localhost:4566", # the exposed port
aws_access_key_id="test",
aws_secret_access_key="test",
)
Inside Docker Compose:
api → http://localstack:4566 (internal network)
api → http://cache:6379 (internal network)
From your machine:
you → http://localhost:4566 (exposed port)
you → http://localhost:8000 (exposed port)
The updated .env
# .env — Add the LocalStack variables
OPENAI_API_KEY=sk-proj-your-key-here
ENVIRONMENT=development
LOG_LEVEL=debug
CACHE_TTL=3600
MODEL_NAME=gpt-4o-mini
MAX_TOKENS=500
# LocalStack
AWS_ENDPOINT_URL=http://localstack:4566
AWS_ACCESS_KEY_ID=test
AWS_SECRET_ACCESS_KEY=test
AWS_DEFAULT_REGION=us-east-1
Exercises
Exercise 1: Compose with LocalStack from scratch
Create a docker-compose.yml that has only LocalStack with S3 and Lambda enabled. Start it, verify the health check, create a bucket, and list the buckets with awslocal.
See solution
# docker-compose.yml
services:
localstack:
image: localstack/localstack:latest
ports:
- "4566:4566"
environment:
- SERVICES=s3,lambda
- DEBUG=0
volumes:
- localstack_data:/var/lib/localstack
- /var/run/docker.sock:/var/run/docker.sock
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:4566/_localstack/health"]
interval: 10s
timeout: 5s
retries: 5
start_period: 15s
volumes:
localstack_data:
# Start
docker compose up -d
# Wait for the health check
docker compose ps
# Wait until localstack shows "Up (healthy)"
# Verify health
curl http://localhost:4566/_localstack/health
# Create bucket
awslocal s3 mb s3://my-first-bucket
# List buckets
awslocal s3 ls
# Output: 2026-03-08 ... my-first-bucket
Exercise 2: Automatic initialization script
Create an init script that runs automatically when LocalStack starts. The script should create two buckets (ai-models and ai-results) and confirm with a listing.
See solution
# init-scripts/setup.sh
#!/bin/bash
set -e
echo "=== Initializing LocalStack ==="
awslocal s3 mb s3://ai-models
echo "Bucket ai-models created"
awslocal s3 mb s3://ai-results
echo "Bucket ai-results created"
echo "=== Available buckets ==="
awslocal s3 ls
echo "=== Initialization complete ==="
# docker-compose.yml — add the init volume
services:
localstack:
image: localstack/localstack:latest
ports:
- "4566:4566"
environment:
- SERVICES=s3,lambda
volumes:
- localstack_data:/var/lib/localstack
- /var/run/docker.sock:/var/run/docker.sock
- ./init-scripts:/etc/localstack/init/ready.d
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:4566/_localstack/health"]
interval: 10s
timeout: 5s
retries: 5
start_period: 15s
volumes:
localstack_data:
# Make the script executable
chmod +x init-scripts/setup.sh
# Start
docker compose up -d
# Check the logs to see the initialization
docker compose logs localstack | grep "Bucket"
# Verify the buckets
awslocal s3 ls
# ai-models
# ai-results
Exercise 3: Verification with boto3
Write a Python script that connects to LocalStack, creates a bucket, uploads a JSON file with an AI model's metadata, downloads it, and verifies that the content is identical.
See solution
# verify_localstack.py
import json
import boto3
s3 = boto3.client(
"s3",
endpoint_url="http://localhost:4566",
aws_access_key_id="test",
aws_secret_access_key="test",
region_name="us-east-1",
)
bucket = "verification-test"
key = "models/gpt4o-mini-config.json"
model_config = {
"model": "gpt-4o-mini",
"max_tokens": 500,
"temperature": 0.7,
"system_prompt": "You are a helpful and concise assistant.",
"version": "2026-03",
}
print("1. Creating bucket...")
s3.create_bucket(Bucket=bucket)
print("2. Uploading file...")
s3.put_object(
Bucket=bucket,
Key=key,
Body=json.dumps(model_config),
ContentType="application/json",
)
print("3. Downloading file...")
response = s3.get_object(Bucket=bucket, Key=key)
downloaded = json.loads(response["Body"].read().decode("utf-8"))
print("4. Verifying content...")
assert downloaded == model_config, "The content doesn't match"
print("5. Listing objects...")
objects = s3.list_objects_v2(Bucket=bucket)
for obj in objects.get("Contents", []):
print(f" {obj['Key']} ({obj['Size']} bytes)")
print("\nVerification complete — LocalStack + boto3 working")
python verify_localstack.py
# 1. Creating bucket...
# 2. Uploading file...
# 3. Downloading file...
# 4. Verifying content...
# 5. Listing objects...
# models/gpt4o-mini-config.json (123 bytes)
#
# Verification complete — LocalStack + boto3 working
Exercise 4: Complete multi-service Compose
Extend the Module 2 Docker Compose (FastAPI + Redis) by adding LocalStack. Configure the environment variables so FastAPI can connect to LocalStack using the service's hostname. Verify that all three services are healthy.
See solution
# docker-compose.yml
services:
api:
build:
context: ./api
ports:
- "8000:8000"
env_file:
- .env
environment:
- REDIS_URL=redis://cache:6379
- AWS_ENDPOINT_URL=http://localstack:4566
- AWS_ACCESS_KEY_ID=test
- AWS_SECRET_ACCESS_KEY=test
- AWS_DEFAULT_REGION=us-east-1
depends_on:
cache:
condition: service_healthy
localstack:
condition: service_healthy
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 15s
cache:
image: redis:7-alpine
command: redis-server --appendonly yes --maxmemory 128mb --maxmemory-policy allkeys-lru
volumes:
- redis_data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 3
localstack:
image: localstack/localstack:latest
ports:
- "4566:4566"
environment:
- SERVICES=s3,lambda
- DEBUG=0
- LAMBDA_EXECUTOR=docker
- DOCKER_HOST=unix:///var/run/docker.sock
volumes:
- localstack_data:/var/lib/localstack
- /var/run/docker.sock:/var/run/docker.sock
- ./init-scripts:/etc/localstack/init/ready.d
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:4566/_localstack/health"]
interval: 10s
timeout: 5s
retries: 5
start_period: 15s
volumes:
redis_data:
localstack_data:
docker compose up -d
docker compose ps
# Verify the three services:
# api Up (healthy)
# cache Up (healthy)
# localstack Up (healthy)
curl http://localhost:8000/health
# {"status": "healthy", ...}
curl http://localhost:4566/_localstack/health
# {"services": {"s3": "available", "lambda": "available"}}
Troubleshooting
"LocalStack won't start — the container exits immediately"
# Check the logs
docker compose logs localstack
# Common cause: port 4566 already in use
lsof -i :4566
# If there's another process, kill it or change the port in Compose
# Common cause: Docker socket not accessible
ls -la /var/run/docker.sock
# It should exist and have read permissions
"curl to /_localstack/health returns connection refused"
# LocalStack takes ~10-15s to start
# Wait and retry
sleep 15
curl http://localhost:4566/_localstack/health
# If it keeps failing, verify the container is running
docker compose ps localstack
docker compose logs localstack --tail 20
"awslocal s3 ls returns a credentials error"
# Configure dummy credentials
aws configure set aws_access_key_id test
aws configure set aws_secret_access_key test
aws configure set region us-east-1
# Or use environment variables
export AWS_ACCESS_KEY_ID=test
export AWS_SECRET_ACCESS_KEY=test
"LAMBDA_EXECUTOR=docker doesn't work on a Mac with Apple Silicon"
# On a Mac with M1/M2/M3, make sure you have Docker Desktop updated
docker --version
# If Lambda fails with the Docker executor, try local
environment:
- LAMBDA_EXECUTOR=local
# Less faithful to AWS but works on all platforms
"Service X doesn't appear in the health check"
# Verify you included it in SERVICES
# If SERVICES=s3,lambda and you request SQS, it won't be available
curl http://localhost:4566/_localstack/health
# Add the service to the SERVICES variable and restart
Summary
- LocalStack integrates into Docker Compose as one more service, alongside your app and Redis. It's not a separate tool.
- A single port (4566) exposes all the emulated AWS services. Simple, clean.
awslocalis your CLI for interacting with LocalStack — the same commands asaws, minus the--endpoint-url.- Dummy credentials (
test/test) satisfy the CLI's requirement. LocalStack Community doesn't validate credentials. - Init scripts in
/etc/localstack/init/ready.d/automate the setup (create buckets, functions) on startup. - Inside the Compose, services communicate by hostname (
http://localstack:4566). From your machine, you usehttp://localhost:4566. - SERVICES controls what starts. Only S3 and Lambda for this guide — less is more.
Additional Resources
- LocalStack Docker Compose Setup — Official installation guide with Compose
- LocalStack Configuration — All the configuration variables
- LocalStack Init Hooks — Automatic initialization scripts
- awscli-local GitHub — awslocal documentation
- LocalStack Lambda Executor — Lambda configuration in LocalStack
- Docker Compose Networking — How services communicate in Compose
- LocalStack Health Endpoint — LocalStack's internal endpoints