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.
  • awslocal is your CLI for interacting with LocalStack — the same commands as aws, 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 use http://localhost:4566.
  • SERVICES controls what starts. Only S3 and Lambda for this guide — less is more.

Additional Resources

  1. LocalStack Docker Compose Setup — Official installation guide with Compose
  2. LocalStack Configuration — All the configuration variables
  3. LocalStack Init Hooks — Automatic initialization scripts
  4. awscli-local GitHub — awslocal documentation
  5. LocalStack Lambda Executor — Lambda configuration in LocalStack
  6. Docker Compose Networking — How services communicate in Compose
  7. LocalStack Health Endpoint — LocalStack's internal endpoints