:full image for AthenaApril 27, 2026 · v1.3.16
1.3.16 is a hardening release. Twelve account-scoped state dicts that were silently disappearing on warm-boot are now persisted, ten list endpoints stop returning NextToken: null (which Java and Go pagination clients would loop on), CloudFormation Lambdas backed by S3 finally execute their actual code instead of returning a mock response, and there's a new :full Docker tag with DuckDB, PostgreSQL, and MySQL drivers in one image.
ministackorg/ministack:full — DuckDB-backed Athena, no source buildsThe default image is Alpine-based, which keeps it small (~110 MB) but also keeps it free of manylinux-only wheels. DuckDB and psycopg2-binary ship manylinux but not musllinux; on Alpine they fall back to slow source builds or silently disable themselves. Athena's DuckDB engine has been hitting that pain since the engine landed.
1.3.16 publishes a Debian/glibc variant alongside the regular image:
docker pull ministackorg/ministack:full
It's a strict superset of the regular image — same SFTP support, same env-var contract, same healthcheck — plus duckdb, psycopg2-binary, and pymysql. The :full tag tracks the latest release; pinned variants are also published as :1.3.16-full and :1.3-full. The regular :latest and :1.3.16 tags are unchanged.
The full image self-identifies via /_ministack/health:
{ "edition": "full", "version": "1.3.16", "services": { ... } }
PERSIST_STATE=1 is the knob that makes MiniStack survive a container restart. The contract is simple: every service module exposes get_state() and restore_state(), and the persistence layer round-trips them through JSON.
The contract was incomplete. Twelve module-level AccountScopedDicts were being mutated by public APIs but missing from one or both halves of the persistence path. Most landed in 1.3.16:
_resource_policies — aws_secretsmanager_secret_policy silently disappeared on every restart._consumers — aws_kinesis_stream_consumer came back as ResourceNotFoundException after warm-boot._attributes, SNS _platform_applications and _platform_endpoints — same shape._destinations, _metric_filters, _queries — log destinations and metric filters wiped on restart._event_bus_policies, _connections, _api_destinations — every aws_cloudwatch_event_connection and aws_cloudwatch_event_api_destination lost._parameter_history and _tags — GetParameterHistory returned empty after warm-boot._kinesis_positions and _dynamodb_stream_positions — every restart replayed event-source-mapping streams from StartingPosition. With TRIM_HORIZON ESMs, the entire backlog re-played on every container restart — a real at-least-once-delivery violation.There was also a subtler bug in SQS _queue_name_to_url: the dict was snapshotted via dict(asd) instead of copy.deepcopy(asd). AccountScopedDict.__iter__ only yields the current request's keys, so multi-tenant deployments lost every other tenant's queue-name mappings on shutdown serialisation.
Big thanks to @bognari for pushing through most of this audit.
A class of bugs easy to miss in Python-only testing: AWS SDKs differ in how they handle null and PascalCase. boto3 strips nulls client-side and is forgiving about case; Java SDK v2 and Go SDK v2 are stricter.
NextToken: null returned on ten list endpoints across ses_v2, apigatewayv2, and apigatewayv1. boto3 silently dropped them; Java/Go pagination loops checking if NextToken in response ran forever. The key is now omitted (matching real AWS) when there's no next page. AppSync's GraphQL {items, nextToken} shape is intentionally unchanged — null is idiomatic for nullable GraphQL fields.MD5OfMessageAttributes. Recent work added MessageAttributes to the delivered SQS message but skipped the matching MD5 digest. Java SDK v2 verifies it. Now computed at delivery time. Original SNS-attrs work contributed by @arischow.AWS::ApiGatewayV2::Integration physical ID. Returned {apiId}/{integrationId}; CDK and Terraform substitute Ref into route Targets, which then failed lookup at request time. Every CFN-deployed HTTP API returned 500 No integration configured for every request. Now matches real AWS, with a defensive prefix-strip so legacy stacks built against the broken provisioner still resolve. Contributed by @hiddengearz.A quiet showstopper for CDK users. The CloudFormation provisioner for AWS::Lambda::Function accepted Code.S3Bucket / Code.S3Key and stored them as metadata on the function record — but never actually fetched the bytes. Every CFN-deployed Lambda backed by S3 had code_zip = None and returned a hardcoded "Mock response - no code deployed" on invocation. Inline ZipFile was unaffected; the S3 path simply never worked.
1.3.16 fixes the provisioner to actually call the Lambda S3 helper. While we were there, Code.S3ObjectVersion is now threaded end-to-end (CreateFunction, UpdateFunctionCode, PublishLayerVersion, and the CFN provisioner) — so Terraform aws_lambda_function.s3_object_version and CDK Code.fromBucket(..., objectVersion=...) deploy the pinned bytes instead of silently picking up the latest.
resource "aws_s3_object" "lambda_zip" {
bucket = aws_s3_bucket.deploy.id
key = "fn.zip"
source = "build/fn.zip"
etag = filemd5("build/fn.zip")
}
resource "aws_lambda_function" "fn" {
function_name = "my-fn"
role = aws_iam_role.fn.arn
handler = "index.handler"
runtime = "python3.12"
s3_bucket = aws_s3_object.lambda_zip.bucket
s3_key = aws_s3_object.lambda_zip.key
s3_object_version = aws_s3_object.lambda_zip.version_id
}
BIND_HOST env var to configure the listen interface — BIND_HOST=127.0.0.1 ministack restricts the listener to loopback for pip install users on shared dev machines (contributed by @mattwang44).CreateFunction / UpdateFunctionCode / PublishLayerVersion accepted oversize zips and failed at invocation time; all three now reject up-front with InvalidParameterValueException, matching AWS.S3_PERSIST=1 capped versioned object bodies at 10 MB and crashed GetObject(VersionId) with a 500 on larger multipart uploads. Bodies now persist to disk (in-memory record drops the body, on-demand reads stream back), versioned reads return the persisted bytes, and disk writes go through atomic tmp+rename with mode 0o600 / dir 0o700 plus a path-traversal guard._auth_codes lost across warm-boot. _auth_codes had a 5-minute TTL but was declared "ephemeral, not persisted" — any in-flight hosted-UI sign-in straddling a warm-boot was silently invalidated. Now wired into get_state / restore_state. Contributed by @bognari.restore_state but never invoked it on import. Wired in. Contributed by @bognari.load_state block never fired. Contributed by @bognari.GetCertificate body / chain fidelity, plus a private-key disk-leak scrub on RequestCertificate. Contributed by @bognari.PutIntegration dropped contentHandling; CONVERT_TO_TEXT / CONVERT_TO_BINARY payload translation silently no-op'd. Contributed by @bognari.apigateway v1 / v2 get_state() now deep-copies, secretsmanager._delete_secret(force=True) cleans up orphan policies, and acm._list_certificates omits NextToken when there's no next page. Contributed by @bognari./_ministack/health reported version: dev in the published Docker image (pip is stripped from the runtime, so the importlib.metadata fallback fired). Now reads from a build-arg env var.# Regular image (Alpine, ~110 MB) docker pull ministackorg/ministack:1.3.16 # Full image (Debian + DuckDB + psycopg2 + pymysql, ~360 MB) docker pull ministackorg/ministack:1.3.16-full # pip pip install -U ministack
Full changelog: CHANGELOG.md.
Shipped by the MiniStack community. Contributions credited throughout. GitHub · r/ministack