Overcast is alpha — behaviour and APIs may change between releases. Pin your version and read the changelog before upgrading.

overcast

Getting Started

Testcontainers

Run Overcast from your integration tests with the Testcontainers module for Go: a few lines to start the emulator, wait for readiness, and point an AWS SDK client at it.

Testcontainers starts throwaway Docker containers from test code and tears them down with the test. Overcast ships a first-party Testcontainers module for Go; modules for other languages are planned (#1495 tracks listing them in the Testcontainers catalogue once Overcast reaches v1.0).

The module starts an Overcast container, waits until /_overcast/health answers, and hands back everything an AWS SDK client needs: the endpoint URL, region, account ID, and accepted credentials.

Go

The module lives in this repository as the nested Go module testcontainers/go:

go get github.com/overcast-sh/overcast/testcontainers/go@main

(While Overcast is in alpha the module is installed from main; tagged module releases start alongside v1.0.)

import (
    "context"
    "testing"

    "github.com/aws/aws-sdk-go-v2/aws"
    "github.com/aws/aws-sdk-go-v2/credentials"
    "github.com/aws/aws-sdk-go-v2/service/s3"
    "github.com/testcontainers/testcontainers-go"

    overcast "github.com/overcast-sh/overcast/testcontainers/go"
)

func TestWithOvercast(t *testing.T) {
    ctx := context.Background()

    ctr, err := overcast.Run(ctx, "ghcr.io/overcast-sh/overcast-slim:latest")
    testcontainers.CleanupContainer(t, ctr)
    if err != nil {
        t.Fatal(err)
    }

    endpoint, err := ctr.APIEndpoint(ctx) // e.g. http://localhost:32771
    if err != nil {
        t.Fatal(err)
    }

    client := s3.New(s3.Options{
        BaseEndpoint: aws.String(endpoint),
        Region:       ctr.Region(),
        Credentials:  credentials.NewStaticCredentialsProvider(ctr.AccessKey(), ctr.SecretKey(), ""),
        UsePathStyle: true,
    })
    // ... use the client exactly as against real AWS
}

Which image

  • ghcr.io/overcast-sh/overcast-slim:latest — API only, smallest and fastest to start. The right default for tests.
  • ghcr.io/overcast-sh/overcast:latest — adds the web console (handy when debugging a failing test interactively; pair with WithConsole).

Pin an exact version tag (e.g. ghcr.io/overcast-sh/overcast-slim:0.0.1-alpha.40) in CI — Docker never re-pulls a moving tag it already has, so :latest (or :alpha) can go stale on long-lived runners.

Options

Every standard testcontainers.ContainerCustomizer works (WithEnv for the configuration variables, network and mount options, …). The module adds two of its own:

OptionEffect
WithDockerSocketBind-mounts the host’s Docker socket so container-backed services (Lambda invokes, ECS tasks, RDS/ElastiCache/MSK engines, live EFS) can launch sibling containers. Without it they degrade to metadata-only behaviour.
WithConsoleAlso exposes the web console port (4567); reach it with ConsoleEndpoint. Full image only.
ctr, err := overcast.Run(ctx, "ghcr.io/overcast-sh/overcast:latest",
    overcast.WithDockerSocket(),
    overcast.WithConsole(),
    testcontainers.WithEnv(map[string]string{
        "OVERCAST_DEFAULT_REGION": "eu-west-1",
    }),
)

Container API

MethodReturns
APIEndpoint(ctx)The AWS API endpoint on the mapped port — what AWS_ENDPOINT_URL would hold
ConsoleEndpoint(ctx)The web console URL (requires WithConsole)
Region()The emulator’s effective default region, read back from /_overcast/info
AccountID()The account ID embedded in ARNs
AccessKey() / SecretKey()Credentials the emulator accepts (test/test)

Port mapping caveats

Testcontainers publishes the edge port on a random host port, so the endpoint is http://localhost:<random>, not :4566. Overcast handles the common consequences itself — with the Docker socket mounted it asks the daemon for its own port bindings and rewrites client-facing URLs (SQS queue URLs, split-horizon hostnames) accordingly. What cannot be rewritten are values baked into signed material at issue time, such as a Cognito token’s iss claim: a test that compares those literally needs the container published 1:1 (docker run -p 4566:4566) rather than through Testcontainers’ random ports.

Unless you set a hostname yourself (OVERCAST_HOSTNAME or a LocalStack alias), the module sets OVERCAST_HOSTNAME to the Docker daemon’s host, so returned URLs are dialable from the test process even against a remote daemon.

Other languages: the generic-container pattern

Dedicated modules for other languages are planned (#1495), but nothing about Overcast requires one — every Testcontainers implementation can run it as a generic container today. The recipe is always the same three lines of intent: the image, expose port 4566, and wait for HTTP 200 on /_overcast/health. Then point the SDK at the mapped port with region us-east-1 and credentials test/test (or read the effective region and account from GET /_overcast/info).

Node.js

import { GenericContainer, Wait } from "testcontainers";

const container = await new GenericContainer("ghcr.io/overcast-sh/overcast-slim:latest")
  .withExposedPorts(4566)
  .withWaitStrategy(Wait.forHttp("/_overcast/health", 4566))
  .start();

const endpoint = `http://${container.getHost()}:${container.getMappedPort(4566)}`;

Java

GenericContainer<?> overcast = new GenericContainer<>("ghcr.io/overcast-sh/overcast-slim:latest")
    .withExposedPorts(4566)
    .waitingFor(Wait.forHttp("/_overcast/health").forPort(4566));
overcast.start();

String endpoint = "http://" + overcast.getHost() + ":" + overcast.getMappedPort(4566);

Python

import requests
from testcontainers.core.container import DockerContainer
from testcontainers.core.waiting_utils import wait_container_is_ready

@wait_container_is_ready(requests.ConnectionError, requests.HTTPError)
def wait_for_health(endpoint: str) -> None:
    requests.get(f"{endpoint}/_overcast/health", timeout=2).raise_for_status()

with DockerContainer("ghcr.io/overcast-sh/overcast-slim:latest").with_exposed_ports(4566) as overcast:
    endpoint = f"http://{overcast.get_container_host_ip()}:{overcast.get_exposed_port(4566)}"
    wait_for_health(endpoint)

.NET

var overcast = new ContainerBuilder()
    .WithImage("ghcr.io/overcast-sh/overcast-slim:latest")
    .WithPortBinding(4566, assignRandomHostPort: true)
    .WithWaitStrategy(Wait.ForUnixContainer()
        .UntilHttpRequestIsSucceeded(r => r.ForPort(4566).ForPath("/_overcast/health")))
    .Build();
await overcast.StartAsync();

var endpoint = $"http://{overcast.Hostname}:{overcast.GetMappedPublicPort(4566)}";

For container-backed services (Lambda invokes, ECS tasks, RDS engines, …) add your implementation’s bind-mount option for /var/run/docker.sock — see LAMBDA_DOCKER_SOCKET for the paths each platform uses. Everything else here (image choice, port-mapping caveats, configuration env vars) applies unchanged.

Using the LocalStack Testcontainers modules

A suite already written against a LocalStack Testcontainers module runs on Overcast with the image name changed and nothing else. The tag you name is the one thing to get right, because three of the five modules read it as a LocalStack version number.

ModuleTag to nameWhy
Java org.testcontainers:localstack:latestany other tag is read as < 0.13 and demands a withServices list
Go testcontainers-go/modules/localstack:latestany tag it cannot read as ≥ 0.11 is refused outright
Node @testcontainers/localstackanyno tag parsing
Python testcontainers[localstack]anyno tag parsing
.NET Testcontainers.LocalStacka pinned version:latest throws unless LOCALSTACK_AUTH_TOKEN is set — any value will do

Java

LocalStackContainer overcast = new LocalStackContainer(
    DockerImageName.parse("ghcr.io/overcast-sh/overcast-slim:latest")
        .asCompatibleSubstituteFor("localstack/localstack"));
overcast.start();

URI endpoint = overcast.getEndpoint(); // http://127.0.0.1:<random>

Node.js

import { LocalstackContainer } from "@testcontainers/localstack";

const overcast = await new LocalstackContainer("ghcr.io/overcast-sh/overcast-slim:latest").start();

const endpoint = overcast.getConnectionUri();

Python

from testcontainers.community.localstack import LocalStackContainer

with LocalStackContainer(image="ghcr.io/overcast-sh/overcast-slim:latest") as overcast:
    s3 = overcast.get_client("s3", region_name="us-east-1")

Go

ctr, err := localstack.Run(ctx, "ghcr.io/overcast-sh/overcast-slim:latest")

.NET

var overcast = new LocalStackBuilder("ghcr.io/overcast-sh/overcast-slim:0.0.1-alpha.40").Build();
await overcast.StartAsync();

var endpoint = overcast.GetConnectionString();

What makes this work

  • Ready. — Java, Node and Python block on LocalStack’s readiness line. Overcast prints it verbatim, on stderr, once every listener is bound. It is preceded by an overcast ready line that says what it is, so a reader grepping the logs is not left guessing.
  • /_localstack/health — Go and .NET probe this instead of the log. Overcast already served it in LocalStack’s response shape, so neither module needed anything.
  • /usr/local/bin/docker-entrypoint.sh — the Java module replaces the entrypoint and execs LocalStack’s path. The image answers to it, and ships bash for the module’s generated start script.

What still differs

  • Region and credentials. Each module has its own defaults (us-east-1 and test/test for Java and Go, us-west-1 and testcontainers-localstack for Python). Overcast accepts any credentials and any region, so all of them work — but read them from the container object rather than hard-coding, as you would against LocalStack.
  • SERVICES, EAGER_SERVICE_LOADING, LAMBDA_DOCKER_FLAGS. Set by the modules, recognised and inert here: every service is always on, and Overcast labels the containers it starts itself. The startup log names each one it saw.
  • Random ports. Same caveat as above — see Port mapping caveats.

Overcast’s own Go module is still the better tool for a suite you are writing now: it waits on /_overcast/health, reads back the effective region and account, and has no tag parsing to work around. This section is for the suite you already have.