Docs AWS 101 Blog Services

Documentation

How MiniStack works, how to configure it, and what to expect.

Looking for the full reference? The new documentation hub at /docs/ has per-service pages for all 46 services, the CloudFormation engine reference, Testcontainers modules (Java + Python), and honest per-service limitations. This page is the concise overview.

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 Authorization header's credential scope (e.g. s3, dynamodb, ec2)
  • The X-Amz-Target header (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

VariableDefaultWhat it does
GATEWAY_PORT4566 The port MiniStack listens on. Change this if 4566 conflicts with something else on your machine.
LOG_LEVELINFO Controls log verbosity. Use DEBUG to see every incoming request and its routing. Use WARNING or ERROR to quiet things down in CI.

Persistence

VariableDefaultWhat it does
PERSIST_STATE0 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_PERSIST0 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_PERSIST0 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_SIZE256m 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

VariableDefaultWhat it does
RDS_BASE_PORT15432 Starting host port for RDS database containers. Each new database gets the next port (15432, 15433, ...). Connect your DB client to this port.
REDIS_HOSTredis Hostname for the Redis server used by ElastiCache. In Docker Compose, this is usually the service name of your Redis container.
REDIS_PORT6379 Port for the Redis server.
ELASTICACHE_BASE_PORT16379 Starting host port for ElastiCache containers. Same pattern as RDS — each cluster gets the next available port.

Lambda

VariableDefaultWhat it does
LAMBDA_EXECUTORlocal 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_STRICT0 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_SECONDS300 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_CONCURRENCY0 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)

VariableDefaultWhat 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

VariableDefaultWhat 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_ENGINEauto 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 local development: Enable all three. Your entire state persists across restarts — just like a real AWS account (minus the bill).

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

ErrorMeaning
ResourceNotFoundExceptionThe resource (table, queue, function, etc.) doesn't exist. Usually means you're hitting MiniStack before your setup code runs.
ResourceAlreadyExistsExceptionYou're trying to create something that already exists. If using PERSIST_STATE=1, this often means leftover state from a previous run.
ValidationExceptionInvalid parameters. MiniStack validates required fields and basic types, but is more lenient than AWS on optional fields.
UnknownOperationExceptionThe 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.

Multi-region

MiniStack honors per-request regions. A caller signing with us-east-1 and another signing with eu-west-1 get responses with region-correct ARNs and endpoints — the region is derived from the caller's SigV4 credential scope, not a single server-wide setting. MINISTACK_REGION sets the default for unsigned callers. State is scoped per account; some regional services share the underlying store across regions, so use unique resource names if you exercise two regions from the same account.

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
Tip: Set LAMBDA_DOCKER_NETWORK to your Compose project's network name (usually <folder>_default) so Lambda containers can reach MiniStack at http://ministack:4566.