--- title: "LocalStack now needs an auth token: getting it into Testcontainers without a .env file" author: "Marçal Albert Castellví" published: "2025-07-05T00:00:00.000Z" updated: "2026-10-24T11:01:23.143Z" url: "https://zyvop.com/localstack-now-needs-an-auth-token-getting-it-into-testcontainers-without-a-env-file-nrkw0" canonical: "https://dev.to/macalbert/localstack-integration-tests-without-env-files-b75" tags: ["AWS","localstack","Testing","testcontainers"] --- # LocalStack now needs an auth token: getting it into Testcontainers without a .env file ![Cover image]() Since March 2026, [the LocalStack image won't start without]() `LOCALSTACK_AUTH_TOKEN`. Here's how I feed it to Testcontainers (TypeScript, Python, .NET) and Aspire straight from AWS SSM, with no `.env` file in between. So the test fixture needs a secret before the test environment exists. The usual fix is a generated `.env`: - get the token into a secret store in the first place - generate the file before tests - gitignore it - load it into Docker, Testcontainers, or Aspire - keep local and CI aligned It works. But now there is a file whose only job is to carry a secret into your tests: something to generate, gitignore, keep in sync and never commit by accident. One more moving part that lives next to your real configuration instead of inside it. When a tool expects a file (Docker Compose, shell scripts, legacy tooling), a `.env` is a good bridge. When the consumer is _code_, the bridge is unnecessary. [Testcontainers]() and [Aspire]() start LocalStack from inside your test process, and code can resolve the secret directly: ![Generated .env vs resolving the token in the test fixture]() Nothing to generate, nothing to gitignore, nothing to go stale. This is not meant to replace your secrets manager. It just removes the generated `.env` hop when the consumer is already code. ## The contract One file, committed at the repo root. Paths, not values: ```json { "$config": { "provider": "aws", "profile": "default" }, "LOCALSTACK_AUTH_TOKEN": "/demo/localstack/auth-token", "DATABASE_URL": "/demo/db/url" } ``` Enter fullscreen mode Exit fullscreen mode ![One envilder.json map resolved by .NET, Python, TypeScript and CI]() The [Envilder]() SDK resolves it at test time. Same pattern in every stack. ## TypeScript ```typescript const environment = await Envilder.resolveFile('../envilder.json'); localstack = await new LocalstackContainer('localstack/localstack:stable').withEnvironment(Object.fromEntries(environment)).start(); ``` Enter fullscreen mode Exit fullscreen mode Full test: `typescript-testcontainers/localstack.test.ts` ## Python ```python container = LocalStackContainer("localstack/localstack:stable")container.env.update(Envilder.resolve_file(str(MAP_FILE))) container.waiting_for(LogMessageWaitStrategy(re.compile(r"Ready\.")))DockerContainer.start(container) ``` Enter fullscreen mode Exit fullscreen mode The explicit wait for the `Ready.` log line is there because the built-in wait does not match the consolidated 2026 LocalStack image. Full test: `python-testcontainers/test_localstack.py` ## .NET ```csharp var environment = await Env.ResolveFileAsync("envilder.json"); _container = new LocalStackBuilder("localstack/localstack:stable") .WithEnvironment(new Dictionary(environment)) .Build(); await _container.StartAsync(); ``` Enter fullscreen mode Exit fullscreen mode Full fixture: `dotnet-testcontainers/LocalStackFixture.cs` ## Same pattern, different orchestrator: .NET Aspire In .NET, you might not be using Testcontainers directly. [Aspire]() can also orchestrate containers, and it has its own [LocalStack integration](). The AppHost resolves the token and injects it into the LocalStack resource: ```csharp builder.Configuration["LocalStack:UseLocalStack"] = "true"; var localstack = builder .AddLocalStack("localstack", configureContainer: c => c.ContainerImageTag = "stable") ?? throw new InvalidOperationException("LocalStack is disabled."); foreach (var (key, value) in await Env.ResolveFileAsync("envilder.json")){ localstack.WithEnvironment(key, value);} ``` Enter fullscreen mode Exit fullscreen mode One gotcha: without that `UseLocalStack` config flag, `AddLocalStack` returns `null`. That is the package's fallback-to-real-AWS mechanism. `aspire run` and you have a local AWS, token included, with nothing to export first. And it is just as testable: `Aspire.Hosting.Testing` boots the same AppHost from an xUnit fixture, so the round-trip test works identically to the Testcontainers one. Aspire is not .NET-only either. The same AppHost can orchestrate [Python]() and [JavaScript]() apps next to LocalStack, so a FastAPI service resolving its secrets with the Envilder Python SDK fits in the same graph. Full AppHost and its test suite: `dotnet-aspire/` That is the whole integration. The token travels from AWS SSM into the container's environment without being written to a generated file. When it rotates, I update it in SSM once per environment and every suite picks it up on the next run. ## Why not just call SSM directly? You can. A direct `ssm.getParameter(...)` in the fixture is perfectly reasonable, and if your app is AWS-only and already loads Parameter Store through the native config provider (`builder.Configuration.AddSystemsManager(...)` in .NET), you probably do not need anything else in runtime. Reading secrets is a solved problem. The part that stays implicit with direct calls is the _contract_: which logical keys this project needs, where each one lives, and what has to exist before it boots. That knowledge ends up scattered across C# code, SSM paths, GitHub Actions secrets and a stale README. Nobody has the full list in one place. A committed map surfaces it. It is the list, versioned and reviewable in a PR: ```plaintext LOCALSTACK_AUTH_TOKEN -> /demo/localstack/auth-token DATABASE_URL -> /demo/db/url STRIPE_API_KEY -> /demo/stripe/api-key # new ``` Enter fullscreen mode Exit fullscreen mode Add a key and everyone sees the project grew a dependency. Because the code depends on the logical name and every consumer reads the same map, rotating a value, changing a path, or pointing at a different provider is an edit in one place rather than a hunt across call sites. The consumer still has to _use_ the value it wants; it just never has to learn how to fetch it. In a monorepo, where each service commits its own `envilder.json`, that visibility compounds: you can see at a glance what every service depends on without reading its code. That is the trade: ```plaintext direct SDK call -> resolution logic per call site, contract stays implicit shared contract -> one versioned map, every consumer resolves the same thing ``` Enter fullscreen mode Exit fullscreen mode ![One map for all, all from one map]() One contract, many consumers: the .NET fixture, the Python one, the TypeScript setup and every pipeline all resolve the same committed map. It is the same instinct as infrastructure as code, applied to the secret contract: make the implicit explicit and put it under version control. That is a win on a single stack, before any multi-provider story enters the picture. And if you already centralize this in one config layer, good: that layer _is_ the contract. Envilder just makes it explicit and provider-agnostic instead of hand-rolled per stack. One honest caveat while we are here: removing the `.env` is not a stronger runtime secret boundary. The token still ends up in the container environment because LocalStack needs it there. The improvement is operational: there is no intermediate `.env` file to generate, ignore, sync, clean up or accidentally commit. ## The recursion This is exactly how [Envilder's own test suite]() works. The SDK resolves the token that starts the LocalStack container used to test the SDK against an emulated SSM. If Envilder cannot fetch its own secret, the suite fails before a single assertion runs. It is the most honest smoke test in the repo. ## CI The mechanism does not change. The secret does. Locally, the `profile` in `$config` resolves my credentials from `~/.aws/credentials`, and the map points to my developer LocalStack token. In GitHub Actions, OIDC through `aws-actions/configure-aws-credentials` feeds the default credential chain, and the map can point to a [CI Auth Token]() stored in SSM. LocalStack developer tokens are not meant for pipelines, so CI should use a CI-specific token. Same contract, different credentials, different secret. ## Try it Minimal, runnable examples live in [**envilder-examples**](), one folder per stack and orchestrator: - `typescript-testcontainers` - `python-testcontainers` - `dotnet-testcontainers` - `dotnet-aspire` - `typescript-aspire`: Aspire's [TypeScript AppHost](), where `apphost.ts` resolves the token with the same `@envilder/sdk` Each one resolves the token, boots LocalStack, and does a `SecureString` round-trip against emulated SSM. Seeding the token is one command. That is the "get the token into a secret store" step from the top of this post, handled without any manual setup: ```bash npx envilder --push --key=LOCALSTACK_AUTH_TOKEN --value= --secret-path=/demo/localstack/auth-token ``` Enter fullscreen mode Exit fullscreen mode ## Where this came from Envilder started as a fix for my own problem: I wanted the secret mapping in Git and the values out of it, resolved by whatever consumes them. This post is one concrete case where that helped, nothing more. If it turns out other people hit the same wall, there is obvious room to grow it (validating a map in CI, diffing a map against the store, and so on), but that is speculative and feedback-driven, not a promised roadmap. For now it does one thing, and this is that one thing. ## Takeaway ```plaintext secret values outside Git secret mappings inside Git resolution at runtime no generated file when the consumer is code one contract across every call site ``` Enter fullscreen mode Exit fullscreen mode `.env` files are the right bridge when a tool needs a file. Test fixtures are code. Let them resolve secrets through the same contract as everything else. ## Links - Previous post (Docker Compose + generated `.env`): [How I run LocalStack without committing LOCALSTACK\_AUTH\_TOKEN]() - Example repo: [https://github.com/macalbert/envilder-examples]() - Envilder: [https://envilder.com/]() - Testcontainers: [https://testcontainers.com/]() - LocalStack Auth Token docs: [https://docs.localstack.cloud/aws/getting-started/auth-token/]() _Thanks again to the_ [_LocalStack_]() _team for supporting Envilder through their Open Source program._