--- title: "LocalStack now requires an auth token: keeping it out of Git with Docker Compose" author: "Marçal Albert Castellví" published: "2026-06-09T00:00:00.000Z" updated: "2026-10-24T11:01:06.928Z" url: "https://zyvop.com/localstack-now-requires-an-auth-token-keeping-it-out-of-git-with-docker-compose-ar58h" canonical: "https://dev.to/macalbert/how-i-run-localstack-without-committing-localstackauthtoken-34gk" tags: ["AWS","localstack","Docker","security"] --- # LocalStack now requires an auth token: keeping it out of Git with Docker Compose ![Cover image]() ## How I run LocalStack without committing LOCALSTACK\_AUTH\_TOKEN > **Updated October 2026:** This article now includes `envilder exec`, Azure Key Vault, and LocalStack for Azure. The original `.env` workflow is still here, just no longer the default approach I recommend. Since [LocalStack for AWS 2026.03.0](), starting the AWS emulator requires an auth token. With Docker Compose, that means supplying `LOCALSTACK_AUTH_TOKEN`. Fair enough. What I didn't want was another secret pasted into a Compose file, copied between scripts, or remembered as an undocumented `export` command. I wanted the repository to explain what the project needs, without containing the actual credential. I ran into this while building [Envilder](). The solution turned out to be slightly recursive. We'll get to that. ## The pattern The part I want in Git is the answer to this question: > Where does this project get `LOCALSTACK_AUTH_TOKEN`? Envilder reads that answer from a JSON mapping and retrieves the value from AWS SSM Parameter Store or Azure Key Vault. It can then either start a command with the secret in its environment (**exec mode**) or generate a local `.env` file (**pull mode**). Same tool, same mapping, two ways to deliver the value. ![Envilder resolves secrets from AWS SSM or Azure Key Vault and supplies them to Docker Compose through exec or pull mode]() For starting LocalStack, I now prefer exec mode. No need to create a file just so another process can read it. ## Choose where the token lives Pick one of these configurations for your `envilder.json`. You don't need both clouds. **With AWS SSM Parameter Store:** ```json { "$config": { "provider": "aws", "profile": "default" }, "LOCALSTACK_AUTH_TOKEN": "/envilder/localstack/auth-token" } ``` Enter fullscreen mode Exit fullscreen mode Store your token at that parameter path as a `SecureString`, and give your AWS identity permission to read it. **With Azure Key Vault:** ```json { "$config": { "provider": "azure", "vaultUrl": "https://my-development-vault.vault.azure.net" }, "LOCALSTACK_AUTH_TOKEN": "localstack-auth-token" } ``` Enter fullscreen mode Exit fullscreen mode Store the token as a secret named `localstack-auth-token` in that vault. For local development, Envilder can use your `az login` session, provided your identity has permission to read the secret. In both cases, the mapping contains a reference, not the credential. That's what goes into Git. You'll also need Docker Compose and Node.js 22.12+; the [setup guide]() covers authentication. One important detail: [Developer Auth Tokens are personal](). Each developer must resolve their own token, for example through separate accounts or vaults, or a per-user mapping. CI needs a separate CI Auth Token. A shared setup must not become a shared personal credential. ## Let Compose do the rest Here's a minimal `compose.yaml` for LocalStack for AWS: ```yaml services:localstack: image: localstack/localstack:stable ports: - "127.0.0.1:4566:4566" environment: LOCALSTACK_AUTH_TOKEN: "${LOCALSTACK_AUTH_TOKEN:?Start Compose through envilder exec}" ``` Enter fullscreen mode Exit fullscreen mode Then run: ```bash npx envilder exec -- docker compose up -d ``` Enter fullscreen mode Exit fullscreen mode That command works with either mapping above. Envilder resolves the token and starts Docker Compose with it in the process environment. [Compose interpolates the variable]() and passes it to LocalStack. No manual export. No `.env` generated by Envilder. And if the secret is missing, empty, or unreadable, [exec mode]() refuses to start Compose rather than hoping for the best. This is a minimal startup example. Add service-specific ports or Docker socket access only when your workload needs them, and pin a tested image version for reproducible CI runs. ### And LocalStack for Azure? For the same basic setup, replace the image with: ```yaml image: localstack/localstack-azure:latest ``` Enter fullscreen mode Exit fullscreen mode [LocalStack for Azure]() also uses `LOCALSTACK_AUTH_TOKEN` and port `4566` by default. The same token can activate either emulator when your account has the corresponding access. At the time of this update, Azure is in private preview and requires access to be enabled. **Where you store the token and which emulator you run are independent choices.** Azure Key Vault can supply the token for LocalStack for AWS. AWS SSM can supply it for LocalStack for Azure. Just use a real cloud secret store for this bootstrap credential. Asking an emulated Key Vault for the token needed to start that same emulator is a chicken-and-egg problem. ## Still need a .env file? The original approach remains available: ```bash npx envilder --map=envilder.json --envfile=.env docker compose up -d ``` Enter fullscreen mode Exit fullscreen mode Add `.env` to `.gitignore`. Use this when a tool genuinely needs a file; otherwise, exec mode saves a step and avoids another plaintext copy. When switching, remove any old secret-bearing `.env` you no longer need. Exec mode doesn't delete it, and Compose can still load it independently. ## What this does not protect **No generated** `.env` **does not mean the token never touches disk.** Docker retains container environment variables in its configuration metadata. Someone with sufficient Docker access can read them through `docker inspect`. Likewise, `docker compose config` can print resolved values, so don't paste its output into a public issue without checking it. The command you start, and its child processes, can also read the injected secrets. Exec mode is not a sandbox, and it resolves values once at startup rather than continuously refreshing them. The improvement is specific: one fewer plaintext secret file in the working directory. Useful, but not magic. ## The slightly meta part I originally ran into this while working on Envilder's integration tests. Envilder uses LocalStack. LocalStack needs a token. So I used Envilder to resolve the token needed to start the LocalStack environment used to test Envilder. At that point, the meme was unavoidable. ![Envilder uses Envilder to resolve the LocalStack token needed to test Envilder]() Originally, that meant generating a `.env` file. Exec mode now offers another way to deliver the same secret without changing the mapping. The tool became its own first customer, which is always a useful kind of feedback. ## Why not lstk, direnv, 1Password, or chamber? For running LocalStack by itself, I'd start with [lstk](). It handles login and normally stores the token in the system keyring. You don't need to add a cloud secret store just to try LocalStack. There are good options for larger workflows too. [direnv]() can run your secret-retrieval commands when you enter a directory. [1Password]() `op run` injects secrets from 1Password. [chamber]() already handles AWS SSM and command execution. Envilder isn't claiming to have invented environment injection. What I'm building around is the explicit `variable name -> secret path` mapping: one JSON contract format shared by the CLI, GitHub Action, and .NET, Python, and Node.js SDKs, using AWS SSM or Azure Key Vault. If a small script already solves your problem, keep it. The contract becomes useful when you would otherwise maintain the same mapping in several different places. ## Takeaway The original goal was to keep the token out of Git. Exec mode goes a little further: I no longer need to generate a local `.env` just to hand that token to Docker Compose. The secret stays managed in my cloud provider. The repository describes where to find it. LocalStack receives the variable it already expects. That's why I'm building [Envilder](): not to create another place to store secrets, but to make resolving them explicit and repeatable. How are you handling this in your own projects? I'd be interested in hearing where your setup differs, especially where this approach wouldn't fit.