Happy Dussehra! May good always win. Happy Diwali! Wishing you a bright year ahead. Merry Christmas! Warm wishes from Vizag. Happy New Year! Here’s to a great year ahead.

Archive · AWS

LocalStack 4.0: A Local AWS Dev Environment with Docker and awslocal

Run LocalStack in Docker Compose, use awslocal and SDKs against it, seed resources with init hooks, and know where a local emulator stops being accurate.

3 min read

  • LocalStack 4.0
  • awslocal
  • Docker 27
  • Docker Compose
  • AWS SDK

Developing against a real AWS account is slow, costs money, and makes experiments risky. LocalStack emulates many AWS APIs on your machine behind one endpoint, so you can build and test S3, SQS, SNS, DynamoDB, Lambda and more offline.

Versions in use on this date. LocalStack 4.0 was released in November 2024 (the Community image is free and open source; some services and features, such as persistence and IAM enforcement, belong to paid tiers). Docker Engine 27 is current. Pin an exact image tag instead of latest so your team and CI see identical behaviour.

Architecture of the local loop

flowchart TB
  DEV[Developer code, tests and CLI] -->|http localhost 4566| LS
  subgraph LS[LocalStack container]
    GW[Edge gateway port 4566] --> S3[S3]
    GW --> SQS[SQS and SNS]
    GW --> DDB[DynamoDB]
    GW --> LAM[Lambda]
    GW --> OTH[Other emulated services]
  end
  LAM -->|spawns execution containers| DOCK[Docker socket on host]
  LS --- VOL[(Volume localstack-data<br/>temporary state)]
  INIT[Init hooks<br/>ready.d scripts] -->|create buckets and queues| GW

Every service is reached on the single edge port 4566. Lambda functions run in sibling containers created through the Docker socket, which is why that socket is mounted.

Docker Compose

services:
  localstack:
    image: localstack/localstack:4.0
    container_name: localstack
    ports:
      - "127.0.0.1:4566:4566"            # edge endpoint, localhost only
      - "127.0.0.1:4510-4559:4510-4559"  # external service ports
    environment:
      - SERVICES=s3,sqs,sns,dynamodb,lambda,iam,sts
      - DEBUG=0
      - LAMBDA_EXECUTOR=docker
    volumes:
      - "./localstack-data:/var/lib/localstack"
      - "./init:/etc/localstack/init/ready.d"
      - "/var/run/docker.sock:/var/run/docker.sock"
docker compose up -d
curl -s http://localhost:4566/_localstack/health | jq '.services'

Mounting docker.sock gives the container control of the host’s Docker daemon, which is acceptable on a developer laptop but should be thought about on shared CI machines. Limit SERVICES to what you need to cut start-up time and memory.

awslocal

awslocal is the AWS CLI with the endpoint and test credentials already set:

pipx install awscli-local        # or: pip install awscli-local awscli
awslocal s3 mb s3://demo-bucket
awslocal s3 cp ./hello.txt s3://demo-bucket/
awslocal s3 ls s3://demo-bucket
awslocal sqs create-queue --queue-name orders
awslocal dynamodb create-table --table-name users \
  --attribute-definitions AttributeName=id,AttributeType=S \
  --key-schema AttributeName=id,KeyType=HASH --billing-mode PAY_PER_REQUEST

Without awslocal, point the normal tools at the endpoint:

export AWS_ACCESS_KEY_ID=test AWS_SECRET_ACCESS_KEY=test AWS_DEFAULT_REGION=ap-south-1
aws --endpoint-url=http://localhost:4566 s3 ls

In SDK code, set the endpoint per environment, for example in JavaScript:

import { S3Client } from "@aws-sdk/client-s3";
const s3 = new S3Client({
  region: "ap-south-1",
  endpoint: process.env.AWS_ENDPOINT_URL,        // http://localhost:4566 locally, unset in AWS
  forcePathStyle: true,                          // bucket in path, not subdomain
});

Keep the endpoint in configuration, never hard-coded, so the same code runs unchanged on real AWS.

Seed resources automatically

Scripts placed in ./init run each time LocalStack becomes ready:

#!/bin/bash
# init/01-resources.sh
awslocal s3 mb s3://demo-bucket
awslocal sqs create-queue --queue-name orders
awslocal sns create-topic --name order-events

Make them executable (chmod +x) and idempotent where possible. Free-tier state is mostly lost when the container is removed, so treat the emulator as disposable and recreate everything from scripts or Terraform (next post).

A Lambda in 60 seconds

echo 'exports.handler = async (e) => ({ ok: true, input: e });' > index.js && zip fn.zip index.js
awslocal lambda create-function --function-name hello --runtime nodejs20.x \
  --handler index.handler --zip-file fileb://fn.zip \
  --role arn:aws:iam::000000000000:role/lambda-role
awslocal lambda invoke --function-name hello --payload '{"a":1}' --cli-binary-format raw-in-base64-out out.json && cat out.json

What LocalStack does not prove

  • IAM is not enforced in the free edition, so a policy that fails in real AWS may pass locally. Test IAM in a real sandbox account.
  • Behaviour, limits and error messages of some services are approximations; Lambda cold starts, throttling and eventual consistency differ.
  • Not every API or newer feature is implemented. Check the coverage page for each service you depend on.
  • Region, account ID (000000000000) and generated ARNs are local.

Use LocalStack for fast feedback and integration tests, then run a smaller set of tests against a real non-production AWS account before release.

Troubleshooting

  • Connection refused: docker compose ps, then docker logs localstack; wait for the “Ready.” line.
  • Lambda cannot start: the Docker socket is not mounted or Docker is not running.
  • Application inside another container cannot reach localhost:4566: use the Compose service name (http://localstack:4566) on the same network.