{"schemaVersion":"1.0","type":"TechArticle","types":["Article","TechArticle"],"slug":"localstack-now-needs-an-auth-token-getting-it-into-testcontainers-without-a-env-file-nrkw0","url":"https://zyvop.com/localstack-now-needs-an-auth-token-getting-it-into-testcontainers-without-a-env-file-nrkw0","title":"LocalStack now needs an auth token: getting it into Testcontainers without a .env file","subtitle":null,"tldr":"Since March 2026, the LocalStack image won't start without LOCALSTACK_AUTH_TOKEN. Here's how I feed...","keywords":["AWS","localstack","Testing","testcontainers","DevOps","LocalStack authentication without .env files"],"entities":["Marçal Albert Castellví","Principal Software Engineer","AWS","localstack","Testing","testcontainers","DevOps","LocalStack authentication without .env files","ZyVOP"],"keyTakeaways":["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."],"headings":["The contract","TypeScript","Python",".NET","Same pattern, different orchestrator: .NET Aspire","Why not just call SSM directly?","The recursion","CI","Try it","Where this came from","Takeaway","Links"],"outboundLinks":["https://blog.localstack.cloud/localstack-for-aws-release-2026-03-0/","https://testcontainers.com/","https://aspire.dev/","https://envilder.com/","https://github.com/localstack-dotnet/dotnet-aspire-for-localstack","https://aspire.dev/integrations/frameworks/python/","https://aspire.dev/integrations/frameworks/javascript/","https://github.com/macalbert/envilder","https://docs.localstack.cloud/aws/getting-started/auth-token/","https://github.com/macalbert/envilder-examples","https://devblogs.microsoft.com/aspire/aspire-typescript-apphost/","https://dev.to/macalbert/how-i-run-localstack-without-committing-localstackauthtoken-34gk","https://localstack.cloud/"],"contentText":"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: 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: { \"$config\": { \"provider\": \"aws\", \"profile\": \"default\" }, \"LOCALSTACK_AUTH_TOKEN\": \"/demo/localstack/auth-token\", \"DATABASE_URL\": \"/demo/db/url\" }Enter fullscreen mode Exit fullscreen mode The Envilder SDK resolves it at test time. Same pattern in every stack. 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 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 var environment = await Env.ResolveFileAsync(\"envilder.json\"); _container = new LocalStackBuilder(\"localstack/localstack:stable\") .WithEnvironment(new Dictionary&lt;string, string&gt;(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: builder.Configuration[\"LocalStack:UseLocalStack\"] = \"true\"; var localstack = builder .AddLocalStack(\"localstack\", configureContainer: c =&gt; 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: LOCALSTACK_AUTH_TOKEN -&gt; /demo/localstack/auth-token DATABASE_URL -&gt; /demo/db/url STRIPE_API_KEY -&gt; /demo/stripe/api-key # newEnter 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: direct SDK call -&gt; resolution logic per call site, contract stays implicit shared contract -&gt; one versioned map, every consumer resolves the same thingEnter fullscreen mode Exit fullscreen mode 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: npx envilder --push --key=LOCALSTACK_AUTH_TOKEN --value=&lt;your-token&gt; --secret-path=/demo/localstack/auth-tokenEnter 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 secret values outside Git secret mappings inside Git resolution at runtime no generated file when the consumer is code one contract across every call siteEnter 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.","contentHash":"sha256:d5ea2229adb4646a0c48bd2d47028fa4fed9d361915e4aab3fb1288ca95e2fbf","authorName":"Marçal Albert Castellví","authorUrl":"https://zyvop.com/author/mac","authorSameAs":["https://envilder.com","https://github.com/macalbert","https://linkedin.com/in/linkedin.com/in/macalbert"],"category":"DevOps","tags":["AWS","localstack","Testing","testcontainers"],"audience":"Software engineers and developers building applications with DevOps","tone":"Professional, principal software engineer perspective","readingTimeMinutes":6,"wordCount":1428,"faqs":null,"primaryTopic":"DevOps","publishedAt":"2025-07-05T00:00:00.000Z","updatedAt":"2026-10-24T11:01:23.143Z","canonicalUrl":"https://dev.to/macalbert/localstack-integration-tests-without-env-files-b75"}