ministack README

The project's README from github.com/ministackorg/ministack. As one markdown file: README.md.

MiniStack — Free Open-Source AWS Emulator

MiniStack

Free, open-source local AWS emulator. Free forever.

60+ AWS services on a single port · Multi-account & multi-region · Terraform compatible · Real databases · MIT licensed

GitHub release Build Docker Pulls Docker Image Size License Python

Ministack.org · Docker Hub · LinkedIn


Why MiniStack?

LocalStack recently moved its core services behind a paid plan. If you relied on LocalStack Community for local development and CI/CD pipelines, MiniStack is your free alternative.


Quick Start

# Option 1: PyPI (simplest)
pip install ministack
ministack
# Runs on http://localhost:4566 — use GATEWAY_PORT=XXXX to change

# Option 2: Docker Hub
docker run -p 4566:4566 ministackorg/ministack

# Option 2b: Docker Hub with real infrastructure (RDS, ECS, Lambda containers)
docker run -p 4566:4566 -v /var/run/docker.sock:/var/run/docker.sock ministackorg/ministack

# Option 2c: Full image — Debian/glibc base with DuckDB (Athena), psycopg2, pymysql.
# Larger (~360 MB vs ~110 MB) but enables Athena and native PostgreSQL/MySQL drivers
# that don't ship musllinux wheels. Reports `edition: full` on /_ministack/health.
docker run -p 4566:4566 ministackorg/ministack:full

# Option 3: Clone and build
git clone https://github.com/ministackorg/ministack
cd ministack
docker compose up -d

# Verify (any option)
curl http://localhost:4566/_ministack/health

That's it. No account, no API key, no sign-up.


Internal API

MiniStack exposes internal endpoints for test automation:

# Health check — returns service status
curl http://localhost:4566/_ministack/health

# Reset all state — wipe every service back to empty (useful between test runs)
curl -X POST http://localhost:4566/_ministack/reset

# Reset and re-run init scripts (boot.d + ready.d)
curl -X POST http://localhost:4566/_ministack/reset?init=1

# Runtime config — change service-level settings without restart
curl -X POST http://localhost:4566/_ministack/config \
  -H "Content-Type: application/json" \
  -d '{"lambda_svc.LAMBDA_EXECUTOR": "docker"}'

# Inspect emails sent via SES — returns every message grouped by account
curl http://localhost:4566/_ministack/ses/messages

# Filter by account (12-digit access-key ID used as the account ID)
curl "http://localhost:4566/_ministack/ses/messages?account=000000000000"

# Inspect SQS messages — returns every queue's messages grouped by account
# (includes Body, MessageId, ReceiveCount, VisibleAt, IsVisible, MessageAttributes, FIFO group/dedup)
curl http://localhost:4566/_ministack/sqs/messages

# Filter by account and/or a specific queue
curl "http://localhost:4566/_ministack/sqs/messages?account=000000000000&QueueUrl=http://localhost:4566/000000000000/my-queue"

The reset endpoint is especially useful in CI pipelines and test suites — call it in setUp/beforeEach to get a clean environment for every test without restarting the container. Add ?init=1 to re-run your init scripts after the reset, restoring any resources they create (VPCs, queues, seed data, etc.).

The config endpoint supports these keys:

Key Description
lambda_svc.LAMBDA_EXECUTOR Lambda execution mode (local or docker)
athena.ATHENA_ENGINE Athena query engine (duckdb or mock)
athena.ATHENA_DATA_DIR Directory for Athena DuckDB data files
stepfunctions._sfn_mock_config SFN mock config (AWS SFN Local compatible)
stepfunctions._SFN_WAIT_SCALE Scale factor for Wait state durations and retry sleeps (0 = skip all waits)

To set region or account ID, use environment variables at startup:

docker run -p 4566:4566 \
  -e MINISTACK_REGION=eu-west-1 \
  -e MINISTACK_ACCOUNT_ID=123456789012 \
  ministackorg/ministack

Or use the multi-tenancy feature — a 12-digit access key automatically becomes the account ID (see Multi-Tenancy below).

Also compatible with LocalStack's health endpoint:

curl http://localhost:4566/_localstack/health
curl http://localhost:4566/health

Multi-Tenancy

MiniStack supports lightweight multi-tenancy without any configuration. If the AWS_ACCESS_KEY_ID is a 12-digit number, it is used as the Account ID for all ARN generation. Non-numeric keys (like test) fall back to the MINISTACK_ACCOUNT_ID env var or 000000000000.

# Team A — gets account 111111111111
export AWS_ACCESS_KEY_ID=111111111111
export AWS_SECRET_ACCESS_KEY=test
aws --endpoint-url=http://localhost:4566 sts get-caller-identity
# → { "Account": "111111111111", ... }

# Team B — gets account 222222222222
export AWS_ACCESS_KEY_ID=222222222222
export AWS_SECRET_ACCESS_KEY=test
aws --endpoint-url=http://localhost:4566 sts get-caller-identity
# → { "Account": "222222222222", ... }

All ARNs and resource state (SQS queues, Lambda functions, IAM roles, S3 buckets, DynamoDB tables, etc.) are fully isolated per account. Resources with the same name in different accounts never collide. This allows multiple developers or CI pipelines to share a single MiniStack endpoint with complete tenant isolation — no extra setup needed.

Since 1.4.0 state is additionally isolated per region — see Multi-Region below.

Access Key Account ID Used
111111111111 111111111111
048408301323 048408301323
test 000000000000 (default)
AKIA_TEST_KEY_DO_NOT_USE 000000000000 (default)

Terraform — set access_key in your provider block:

provider "aws" {
  access_key = "048408301323"
  secret_key = "test"
  region     = "us-east-1"
  endpoints { ... }
}

boto3 — pass aws_access_key_id:

boto3.client("s3",
    endpoint_url="http://localhost:4566",
    aws_access_key_id="048408301323",
    aws_secret_access_key="test",
)

Multi-Region

Since 1.4.0, state is isolated per region as well as per account — no configuration needed. The region comes from the SigV4 credential scope (the region_name your SDK or --region your CLI is configured with), so two clients pointed at different regions see fully independent resources, exactly like real AWS:

# Same queue name, two regions — two independent queues
aws --endpoint-url=http://localhost:4566 --region us-east-1 sqs create-queue --queue-name jobs
aws --endpoint-url=http://localhost:4566 --region eu-west-1 sqs create-queue --queue-name jobs

aws --endpoint-url=http://localhost:4566 --region us-east-1 sqs list-queues
# → .../000000000000/jobs   (the us-east-1 queue)
aws --endpoint-url=http://localhost:4566 --region eu-west-1 sqs list-queues
# → .../000000000000/jobs   (a different queue — eu-west-1's)
use1 = boto3.client("dynamodb", endpoint_url="http://localhost:4566",
                    aws_access_key_id="test", aws_secret_access_key="test",
                    region_name="us-east-1")
euw1 = boto3.client("dynamodb", endpoint_url="http://localhost:4566",
                    aws_access_key_id="test", aws_secret_access_key="test",
                    region_name="eu-west-1")

use1.create_table(TableName="users",
    KeySchema=[{"AttributeName": "id", "KeyType": "HASH"}],
    AttributeDefinitions=[{"AttributeName": "id", "AttributeType": "S"}],
    BillingMode="PAY_PER_REQUEST")

use1.list_tables()["TableNames"]   # → ["users"]
euw1.list_tables()["TableNames"]   # → [] — eu-west-1 is independent

Region-isolated services: as of 1.5.3, every regional service is region-isolated except S3 and Aurora DSQL (the isolation migrations rolled out service by service from 1.4.0 to 1.4.12 — see the CHANGELOG for the per-release history; services introduced later, like AWS Config and the Cloud Control API in 1.4.14, were born region-scoped).

Cross-resource references resolve in the referenced ARN's own account and region (SNS→SQS fanout, EventBridge targets, event source mappings), and cross-region references that real AWS rejects return the same errors AWS returns — e.g. invoking a eu-west-1 Lambda from a us-east-1 Step Functions task fails with Functions from 'eu-west-1' are not reachable in this region, exactly as on AWS.

Not yet region-isolated: S3 and Aurora DSQL — these share state across regions within an account (as all services did before 1.4.0); use unique resource names there if your tests exercise two regions, until their isolation lands in a subsequent release. IAM/STS, CloudFront, Route 53, and Organizations are global services on AWS, so their account-scoped shared state is correct, not a gap.

Upgrading with PERSIST_STATE=1: existing state files load and migrate automatically (on-disk format v2 with a version stamp — a newer-format file is refused instead of mis-parsed on downgrade). Each record's region is recovered from its stored ARNs; legacy records that carry no ARN migrate to the default region (MINISTACK_REGION).


Using with AWS CLI

# Option A — environment variables (no profile needed)
export AWS_ACCESS_KEY_ID=test
export AWS_SECRET_ACCESS_KEY=test
export AWS_DEFAULT_REGION=us-east-1

aws --endpoint-url=http://localhost:4566 s3 mb s3://my-bucket
aws --endpoint-url=http://localhost:4566 sqs create-queue --queue-name my-queue
aws --endpoint-url=http://localhost:4566 dynamodb list-tables
aws --endpoint-url=http://localhost:4566 sts get-caller-identity

# Option B — named profile (must pass --profile on every command)
aws configure --profile local
# AWS Access Key ID: test
# AWS Secret Access Key: test
# Default region: us-east-1
# Default output format: json

aws --profile local --endpoint-url=http://localhost:4566 s3 mb s3://my-bucket
aws --profile local --endpoint-url=http://localhost:4566 s3 cp ./file.txt s3://my-bucket/
aws --profile local --endpoint-url=http://localhost:4566 sqs create-queue --queue-name my-queue
aws --profile local --endpoint-url=http://localhost:4566 dynamodb list-tables
aws --profile local --endpoint-url=http://localhost:4566 sts get-caller-identity

awslocal wrapper

chmod +x bin/awslocal
./bin/awslocal s3 ls
./bin/awslocal dynamodb list-tables

Using with boto3

import boto3

# All clients use the same endpoint
def client(service):
    return boto3.client(
        service,
        endpoint_url="http://localhost:4566",
        aws_access_key_id="test",
        aws_secret_access_key="test",
        region_name="us-east-1",
    )

# S3
s3 = client("s3")
s3.create_bucket(Bucket="my-bucket")
s3.put_object(Bucket="my-bucket", Key="hello.txt", Body=b"Hello, MiniStack!")
obj = s3.get_object(Bucket="my-bucket", Key="hello.txt")
print(obj["Body"].read())  # b'Hello, MiniStack!'

# SQS
sqs = client("sqs")
q = sqs.create_queue(QueueName="my-queue")
sqs.send_message(QueueUrl=q["QueueUrl"], MessageBody="hello")
msgs = sqs.receive_message(QueueUrl=q["QueueUrl"])
print(msgs["Messages"][0]["Body"])  # hello

# DynamoDB
ddb = client("dynamodb")
ddb.create_table(
    TableName="Users",
    KeySchema=[{"AttributeName": "userId", "KeyType": "HASH"}],
    AttributeDefinitions=[{"AttributeName": "userId", "AttributeType": "S"}],
    BillingMode="PAY_PER_REQUEST",
)
ddb.put_item(TableName="Users", Item={"userId": {"S": "u1"}, "name": {"S": "Alice"}})

# SSM Parameter Store
ssm = client("ssm")
ssm.put_parameter(Name="/app/db/host", Value="localhost", Type="String")
param = ssm.get_parameter(Name="/app/db/host")
print(param["Parameter"]["Value"])  # localhost

# Secrets Manager
sm = client("secretsmanager")
sm.create_secret(Name="db-password", SecretString='{"password":"s3cr3t"}')

# Kinesis
kin = client("kinesis")
kin.create_stream(StreamName="events", ShardCount=1)
kin.put_record(StreamName="events", Data=b'{"event":"click"}', PartitionKey="user1")

# EventBridge
eb = client("events")
eb.put_events(Entries=[{
    "Source": "myapp",
    "DetailType": "UserSignup",
    "Detail": '{"userId": "123"}',
    "EventBusName": "default",
}])

# Step Functions
sfn = client("stepfunctions")
sfn.create_state_machine(
    name="my-workflow",
    definition='{"StartAt":"Hello","States":{"Hello":{"Type":"Pass","End":true}}}',
    roleArn="arn:aws:iam::000000000000:role/role",
)

# Step Functions — TestState API (test a single state in isolation)
# Note: inject_host_prefix=False prevents boto3 from prepending "sync-" to the hostname
from botocore.config import Config as BotoConfig
sfn_test = client("stepfunctions", config=BotoConfig(inject_host_prefix=False))

result = sfn_test.test_state(
    definition='{"Type":"Pass","Result":{"greeting":"hello"},"End":true}',
    input='{"name":"world"}',
)
print(result["status"])  # SUCCEEDED
print(result["output"])  # {"greeting": "hello"}

# TestState with mock — test error handling without calling real services
result = sfn_test.test_state(
    definition=json.dumps({
        "Type": "Task",
        "Resource": "arn:aws:lambda:us-east-1:000000000000:function:my-fn",
        "Catch": [{"ErrorEquals": ["States.ALL"], "Next": "Fallback"}],
        "End": True
    }),
    input='{}',
    inspectionLevel="DEBUG",
    mock={"errorOutput": {"error": "ServiceError", "cause": "Timeout"}},
)
print(result["status"])  # CAUGHT_ERROR
print(result["nextState"])  # Fallback

# EC2
ec2 = client("ec2")
reservation = ec2.run_instances(
    ImageId="ami-00000001",
    MinCount=1,
    MaxCount=1,
    InstanceType="t3.micro",
)
instance_id = reservation["Instances"][0]["InstanceId"]
print(instance_id)  # i-xxxxxxxxxxxxxxxxx

# Security Groups
sg = ec2.create_security_group(GroupName="my-sg", Description="My SG")
ec2.authorize_security_group_ingress(
    GroupId=sg["GroupId"],
    IpPermissions=[{"IpProtocol": "tcp", "FromPort": 80, "ToPort": 80,
                    "IpRanges": [{"CidrIp": "0.0.0.0/0"}]}],
)

# VPC / Subnet
vpc = ec2.create_vpc(CidrBlock="10.0.0.0/16")
subnet = ec2.create_subnet(
    VpcId=vpc["Vpc"]["VpcId"],
    CidrBlock="10.0.1.0/24",
    AvailabilityZone="us-east-1a",
)

Supported Services

Core Services

Service Operations Notes
S3 CreateBucket, DeleteBucket, ListBuckets, HeadBucket, PutObject, GetObject, DeleteObject, HeadObject, CopyObject, ListObjects v1/v2, DeleteObjects, GetBucketVersioning, PutBucketVersioning, GetBucketEncryption, PutBucketEncryption, DeleteBucketEncryption, GetBucketLifecycleConfiguration, PutBucketLifecycleConfiguration, DeleteBucketLifecycle, GetBucketCors, PutBucketCors, DeleteBucketCors, GetBucketAcl, PutBucketAcl, GetBucketTagging, PutBucketTagging, DeleteBucketTagging, GetBucketPolicy, PutBucketPolicy, DeleteBucketPolicy, GetBucketNotificationConfiguration, PutBucketNotificationConfiguration, GetBucketLogging, PutBucketLogging, ListObjectVersions, CreateMultipartUpload, UploadPart, CompleteMultipartUpload, AbortMultipartUpload, PutObjectLockConfiguration, GetObjectLockConfiguration, PutObjectRetention, GetObjectRetention, PutObjectLegalHold, GetObjectLegalHold, PutBucketReplication, GetBucketReplication, DeleteBucketReplication, GetObjectTagging, PutObjectTagging, DeleteObjectTagging Optional disk persistence via S3_PERSIST=1; Object Lock with retention & legal hold enforcement on delete; object tags are versioned (?tagging&versionId=… reads, writes, and deletes the per-version tag set); Multi-Region Access Points provision from CloudFormation (AWS::S3::MultiRegionAccessPoint) and <alias>.accesspoint.s3-global.amazonaws.com serves the member buckets through the regular S3 path
SQS CreateQueue, DeleteQueue, ListQueues, GetQueueUrl, GetQueueAttributes, SetQueueAttributes, PurgeQueue, SendMessage, ReceiveMessage, DeleteMessage, ChangeMessageVisibility, ChangeMessageVisibilityBatch, SendMessageBatch, DeleteMessageBatch, TagQueue, UntagQueue, ListQueueTags Both Query API and JSON protocol; FIFO queues with deduplication; DLQ support
SNS CreateTopic, DeleteTopic, ListTopics, GetTopicAttributes, SetTopicAttributes, Subscribe, Unsubscribe, ListSubscriptions, ListSubscriptionsByTopic, GetSubscriptionAttributes, SetSubscriptionAttributes, ConfirmSubscription, Publish, PublishBatch, TagResource, UntagResource, ListTagsForResource, CreatePlatformApplication, ListPlatformApplications, GetPlatformApplicationAttributes, SetPlatformApplicationAttributes, CreatePlatformEndpoint, GetEndpointAttributes, SetEndpointAttributes, DeleteEndpoint, DeletePlatformApplication SNS→SQS fanout delivery; SNS→Lambda fanout (asynchronous delivery); FIFO topics with 5-minute deduplication, sequence numbers, content-based deduplication, and subscription validation; mobile-push endpoint lifecycle with device-token dedup and Publish to platform-endpoint TargetArn
DynamoDB CreateTable, UpdateTable, DeleteTable, DescribeTable, ListTables, PutItem, GetItem, DeleteItem, UpdateItem, Query, Scan, BatchWriteItem, BatchGetItem, TransactWriteItems, TransactGetItems, DescribeTimeToLive, UpdateTimeToLive, DescribeContinuousBackups, UpdateContinuousBackups, DescribeEndpoints, TagResource, UntagResource, ListTagsOfResource, EnableKinesisStreamingDestination, DisableKinesisStreamingDestination, DescribeKinesisStreamingDestination, UpdateKinesisStreamingDestination TTL enforced via thread-safe background reaper (60s cadence); DynamoDB Streams — StreamSpecification emits INSERT/MODIFY/REMOVE records on all write operations, respects StreamViewType; Kinesis streaming destinations (aws_dynamodb_kinesis_streaming_destination) fan item mutations out into any Kinesis stream by ARN while the destination is ACTIVE
DynamoDB Streams ListStreams, DescribeStream, GetShardIterator, GetRecords Reads records emitted by the main DynamoDB service via boto3.client("dynamodbstreams") — single

ministack (ministackorg/ministack)

Searching Database…

这是什么?MiniStack 是一款免费的开源 AWS 本地模拟器,可以在你的机器上运行 60 多项 AWS 服务,用于开发和测试。
这是我需要导入的库,还是我单独运行的服务?它是一个你需要运行的独立服务器(通过 Docker 运行),所有 60 多项 AWS 服务都通过同一个端口(4566)对外提供,你的代码可以使用常规的 AWS SDK 或 CLI 与之通信。
我该怎么让它跑起来?运行 Docker 镜像,并将你的 AWS SDK、CLI 或 Terraform provider 指向对外暴露的端点;无需账号或 API 密钥。
我必须用 Docker,还是可以直接运行 Python 代码?该项目以 Docker 镜像形式发布(支持 docker-compose),并使用 Python 编写,因此 Docker 是推荐的运行方式。
所有 AWS 服务都做到了完全隔离并区分区域吗?到 1.5.3 版本时,大多数区域性服务已实现按区域隔离状态,但 S3 和 Aurora DSQL 在同一账号内仍会跨区域共享状态,因此如果要测试多个区域,你需要为这些资源使用唯一的名称。
为什么要开发 MiniStack,而不直接使用 LocalStack?LocalStack 将其核心服务转为付费方案,因此 MiniStack 被开发为一个免费的、采用 MIT 许可证的替代品,用于本地开发和 CI/CD 流水线。
它用的是真实数据库,而不是模拟的吗?是的——RDS 会启动真实的 Postgres/MySQL 容器,ElastiCache 使用真实的 Redis/Valkey,Athena 通过 DuckDB 运行真实的 SQL,ECS 则运行真实的 Docker 容器。
多个开发者或 CI 任务能否安全地共用同一个 MiniStack 实例?可以,内置的多租户功能会让 12 位的 AWS 访问密钥自动成为账号 ID,无需额外设置,即可按账号和区域完全隔离资源与状态。
我能否通过 Bedrock API 运行本地大模型?可以——将 MINISTACK_BEDROCK_PROXY_URL 指向 Ollama、llama.cpp 或 vLLM,就能通过 MiniStack 与 AWS 完全一致的 Bedrock 接口格式获得真实的生成结果。
Size
17.8 MB
Stars
5K
Age
6mo
Commits · 90 Days