Documentation
How MiniStack works, how to configure it, and what to expect.
How it works
MiniStack runs as a single ASGI process on one port (default 4566). Every AWS service shares that port. Requests are routed to the correct service handler based on:
- The
Authorizationheader's credential scope (e.g.s3,dynamodb,ec2) - The
X-Amz-Targetheader (for JSON services like DynamoDB, SFN, EventBridge) - The URL path (for REST services like S3, Route53, API Gateway)
You point any AWS SDK, CLI, or IaC tool to http://localhost:4566 with any credentials. MiniStack doesn't validate credentials — any access key works.
aws --endpoint-url=http://localhost:4566 s3 ls
Configuration
All configuration is via environment variables. Pass them to the Docker container or export them before running the process.
General
| Variable | Default | What it does |
|---|---|---|
GATEWAY_PORT | 4566 |
The port MiniStack listens on. Change this if 4566 conflicts with something else on your machine. |
LOG_LEVEL | INFO |
Controls log verbosity. Use DEBUG to see every incoming request and its routing. Use WARNING or ERROR to quiet things down in CI. |
Persistence
| Variable | Default | What it does |
|---|---|---|
PERSIST_STATE | 0 |
When 1, all service metadata (queues, topics, tables, functions, etc.) is saved to disk on shutdown and restored on startup. Without this, everything is ephemeral — restart MiniStack and it's a clean slate. |
STATE_DIR | /tmp/ministack-state |
Where state files are written. Mount this to a host volume in Docker to keep state between container recreations. |
S3_PERSIST | 0 |
When 1, S3 object data (the actual bytes) is written to disk. Without this, objects only exist in memory. |
S3_DATA_DIR | /tmp/ministack-data/s3 |
Directory for persisted S3 objects. Mount this in Docker if you want uploaded files to survive restarts. |
RDS_PERSIST | 0 |
When 1, RDS database containers use Docker named volumes instead of tmpfs. This means your database data grows dynamically (no size cap) and survives container restarts. When 0, databases run on tmpfs — fast but ephemeral and capped at RDS_TMPFS_SIZE. |
RDS_TMPFS_SIZE | 256m |
Size of the tmpfs mount for RDS containers (only applies when RDS_PERSIST=0). If your tests create large datasets and hit "no space left on device", bump this to 1g or 2g. |
Infrastructure services
| Variable | Default | What it does |
|---|---|---|
RDS_BASE_PORT | 15432 |
Starting host port for RDS database containers. Each new database gets the next port (15432, 15433, ...). Connect your DB client to this port. |
REDIS_HOST | redis |
Hostname for the Redis server used by ElastiCache. In Docker Compose, this is usually the service name of your Redis container. |
REDIS_PORT | 6379 |
Port for the Redis server. |
ELASTICACHE_BASE_PORT | 16379 |
Starting host port for ElastiCache containers. Same pattern as RDS — each cluster gets the next available port. |
Lambda
| Variable | Default | What it does |
|---|---|---|
LAMBDA_EXECUTOR | local |
local runs Lambda functions as subprocesses on the host (fast, no Docker overhead). docker runs each invocation in an isolated container (slower, more realistic). Functions with PackageType: Image or provided runtimes always use Docker regardless of this setting. |
LAMBDA_STRICT | 0 |
Set 1 for AWS-fidelity mode — every invocation must run in a Docker RIE container; in-process fallbacks are disabled. Missing Docker surfaces as Runtime.DockerUnavailable instead of degrading silently. Opt-in because the default install doesn't require Docker. |
LAMBDA_DOCKER_FLAGS | (unset) | Extra docker run flags injected into Lambda containers (matches LocalStack). Supports -e, -v, --dns, --network, --cap-add, -m, --shm-size, --tmpfs, --add-host, --privileged, --read-only. Useful for TLS proxies, custom CA certs, and routed dev networks. |
LAMBDA_WARM_TTL_SECONDS | 300 |
How long an idle warm Lambda container stays in the pool before the reaper evicts it. Raise for long local-dev sessions, lower for CI to free memory faster. |
LAMBDA_ACCOUNT_CONCURRENCY | 0 |
Account-level concurrent-invocation cap (0 = unbounded). Match real AWS by setting to 1000 — used to simulate ConcurrentInvocationLimitExceeded throttles in tests. |
Nested containers (RDS / EKS / ElastiCache / Lambda)
| Variable | Default | What it does |
|---|---|---|
DOCKER_NETWORK | (unset) | Single knob that attaches every container-backed service (RDS, EKS, ElastiCache, Lambda) to the named Docker network. Replaces the old $HOSTNAME auto-detection that silently failed under docker-compose. When set, RDS and ElastiCache Endpoint.Address returns the routable container IP instead of localhost — so Lambda containers on the same network can actually reach them. |
LAMBDA_DOCKER_NETWORK | (unset) | Legacy alias for DOCKER_NETWORK scoped to Lambda only. Still honored as a fallback; prefer DOCKER_NETWORK for new setups. |
MINISTACK_IMAGE_PREFIX | (unset) | Private-registry prefix prepended to every nested image (RDS postgres/mysql/mariadb, ElastiCache redis/memcached, EKS k3s, Lambda runtime images under public.ecr.aws/lambda/*). postgres:15-alpine becomes proxy.corp.net/postgres:15-alpine. Idempotent on already-prefixed images. Testcontainers' hub.image.name.prefix auto-forwards into this variable. |
Other
| Variable | Default | What it does |
|---|---|---|
SFN_MOCK_CONFIG | (unset) | Path to a JSON file defining mock responses for Step Functions Task states. Compatible with the AWS SFN Local mock config format. Lets you test state machines without real Lambda/service calls. |
ATHENA_ENGINE | auto |
auto uses DuckDB if installed, falls back to mock results. duckdb requires DuckDB (fails if missing). mock always returns empty result sets — useful in CI where you don't need real SQL. |
SMTP_HOST | (unset) | SMTP server for SES email delivery (e.g. mailhog:1025). When set, SendEmail/SendRawEmail actually deliver mail to this SMTP server. When unset, emails are stored in memory only — useful for testing without noise. |
Persistence
MiniStack has three independent persistence layers. They don't depend on each other — enable whichever you need.
1. Service state (PERSIST_STATE=1)
Saves the metadata of all services (which queues exist, which Lambda functions are deployed, which DynamoDB tables, etc.) to JSON files in STATE_DIR. On next startup, everything is restored — your queues, functions, and tables are still there.
Does NOT save: S3 object bytes, RDS database data, or in-flight messages.
2. S3 objects (S3_PERSIST=1)
Saves actual file contents (the bytes you PUT into S3) to S3_DATA_DIR. Without this, object data lives only in memory.
3. Database data (RDS_PERSIST=1)
Switches RDS containers from tmpfs (RAM disk, ephemeral) to Docker named volumes (disk-backed, persistent). Your Postgres/MySQL data survives container restarts and has no fixed size cap.
Common setup for CI: Leave all three at
0. Every test run starts clean. Fast and deterministic.
Real infrastructure
Most services are in-memory emulations. These four actually spin up real processes:
RDS — Real Postgres/MySQL
CreateDBInstance pulls and starts a Docker container (postgres:15 or mysql:8). You get a real database on a real port. Connect with psycopg2, mysql-connector, DBeaver — anything.
# After creating the instance: psql -h localhost -p 15432 -U admin -d mydb
ElastiCache — Real Redis
CreateCacheCluster starts a real Redis container. Use any Redis client. Pub/sub works, Lua scripts work, everything works.
redis-cli -p 16379 127.0.0.1:16379> SET hello world OK
ECS — Real Docker containers
RunTask pulls and runs the image from your task definition. Real containers on your Docker socket.
Lambda — Real code execution
Lambda functions actually execute. Python functions run as subprocesses (or in Docker containers if LAMBDA_EXECUTOR=docker). Your handler code runs, returns a response, and can call other MiniStack services.
Testcontainers
MiniStack ships official Testcontainers modules for both Java and Python. They spin up ministackorg/ministack on a random host port, wait for /_ministack/health, and hand your test the endpoint URL + credentials.
Java
<dependency> <groupId>org.ministack</groupId> <artifactId>testcontainers-ministack</artifactId> <version>0.1.4</version> <scope>test</scope> </dependency>
try (MiniStackContainer ms = new MiniStackContainer()) {
ms.start();
S3Client s3 = S3Client.builder()
.endpointOverride(URI.create(ms.getEndpoint()))
.region(Region.of(ms.getRegion()))
.credentialsProvider(StaticCredentialsProvider.create(
AwsBasicCredentials.create(ms.getAccessKey(), ms.getSecretKey())))
.build();
// ... your test
}
Real-infrastructure mode
withRealInfrastructure() mounts the host's Docker socket so RDS, ElastiCache, ECS, and EKS spin up real sidecar containers during the test. The stop() override reaps every MiniStack-labelled container and volume on the host engine — no leaks under Docker or Podman.
try (MiniStackContainer ms = new MiniStackContainer().withRealInfrastructure()) {
ms.start();
// CreateDBInstance will spin up a real postgres container
}
Private-registry setup
Set Testcontainers' hub.image.name.prefix in ~/.testcontainers.properties (or via the TESTCONTAINERS_HUB_IMAGE_NAME_PREFIX env var) and every nested image — the MiniStack image itself, the postgres/redis/k3s sidecars, and Lambda runtimes under public.ecr.aws/lambda/* — routes through your registry mirror. The Java module forwards the prefix into MINISTACK_IMAGE_PREFIX automatically.
Errors
MiniStack returns errors in the same format as AWS — XML for Query/XML services, JSON for JSON services. Status codes match AWS behavior.
Common errors you'll see
| Error | Meaning |
|---|---|
ResourceNotFoundException | The resource (table, queue, function, etc.) doesn't exist. Usually means you're hitting MiniStack before your setup code runs. |
ResourceAlreadyExistsException | You're trying to create something that already exists. If using PERSIST_STATE=1, this often means leftover state from a previous run. |
ValidationException | Invalid parameters. MiniStack validates required fields and basic types, but is more lenient than AWS on optional fields. |
UnknownOperationException | The API action isn't implemented. Check the README for the list of supported operations per service. |
UnsupportedResource (CFN) | CloudFormation template uses a resource type that doesn't have a provisioner. 66 types are supported — others will fail. |
Health check
Hit GET /_ministack/health to verify the server is running. Returns 200 with service status.
curl http://localhost:4566/_ministack/health
Differences from AWS
MiniStack emulates API behavior, not infrastructure. Here's what that means:
No IAM enforcement
All API calls succeed regardless of the IAM policies attached to the caller. IAM resources (users, roles, policies) are stored but never evaluated. Any access key/secret key works.
No real networking
EC2 instances, VPCs, subnets, and security groups exist as metadata. You can't SSH into an instance or route traffic between subnets. These resources exist so Terraform/CDK plans succeed — not to provide real network isolation.
No scheduled execution
EventBridge scheduled rules and cron expressions are stored but never automatically triggered. To test event-driven flows, use PutEvents manually.
No multi-region
MiniStack emulates a single region (default us-east-1). Resources created in different regions all end up in the same store.
CloudFront is metadata-only
Distributions are stored and returned via API, but there's no actual edge caching or content delivery.
EMR/Glue jobs don't execute
Job metadata is tracked but no Spark or Hadoop processing occurs. Useful for testing IaC and orchestration, not for testing data pipelines.
Timestamps
Some services return epoch floats (for Go/Terraform SDK compatibility) where AWS returns ISO strings. This was an intentional choice to maximize compatibility across SDKs.
Account ID
Default account ID is 000000000000. ARNs use this account. Some SDKs send empty account IDs — MiniStack normalizes these automatically.
IaC setup
Terraform
Compatible with AWS Provider v5 and v6. Add endpoint overrides:
provider "aws" {
region = "us-east-1"
access_key = "test"
secret_key = "test"
s3_use_path_style = true
skip_credentials_validation = true
skip_metadata_api_check = true
skip_requesting_account_id = true
endpoints {
s3 = "http://localhost:4566"
dynamodb = "http://localhost:4566"
sqs = "http://localhost:4566"
sns = "http://localhost:4566"
lambda = "http://localhost:4566"
iam = "http://localhost:4566"
ec2 = "http://localhost:4566"
ecs = "http://localhost:4566"
cloudformation = "http://localhost:4566"
route53 = "http://localhost:4566"
cloudwatch = "http://localhost:4566"
secretsmanager = "http://localhost:4566"
ssm = "http://localhost:4566"
kms = "http://localhost:4566"
rds = "http://localhost:4566"
sts = "http://localhost:4566"
}
}
CDK
Use the cdklocal wrapper. It points all CDK operations to localhost:4566:
npm install -g aws-cdk-local cdklocal bootstrap cdklocal deploy
Pulumi
Configure endpoint overrides in Pulumi.dev.yaml:
config:
aws:region: us-east-1
aws:accessKey: test
aws:secretKey: test
aws:s3UsePathStyle: true
aws:skipCredentialsValidation: true
aws:skipMetadataApiCheck: true
aws:skipRequestingAccountId: true
aws:endpoints:
- s3: http://localhost:4566
dynamodb: http://localhost:4566
sqs: http://localhost:4566
lambda: http://localhost:4566
CloudFormation
MiniStack has a built-in CloudFormation engine that supports 66 resource types. Deploy stacks directly via the API:
aws --endpoint-url=http://localhost:4566 cloudformation create-stack \ --stack-name my-stack \ --template-body file://template.yaml
Docker Compose
A ready-to-use setup with full persistence and Lambda Docker support:
services:
ministack:
image: ministackorg/ministack:latest
ports:
- "4566:4566"
environment:
- PERSIST_STATE=1
- S3_PERSIST=1
- RDS_PERSIST=1
- LOG_LEVEL=INFO
- LAMBDA_EXECUTOR=docker
- LAMBDA_DOCKER_NETWORK=myproject_default
volumes:
- ./data/state:/tmp/ministack-state
- ./data/s3:/tmp/ministack-data/s3
- /var/run/docker.sock:/var/run/docker.sock
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:4566/_ministack/health')"]
interval: 10s
timeout: 3s
retries: 3
LAMBDA_DOCKER_NETWORK to your Compose project's network name (usually <folder>_default) so Lambda containers can reach MiniStack at http://ministack:4566.