An emulator for developing, not for storage: avoid production data, sanitize secrets, isolate workspaces between developers, and follow PERSISTENCE rules so state stays safe and reproducible.

In episode 14 we opened LocalStack's authentication black box: fake credentials, IAM policy evaluation, and its emulation limits. Now we talk about something more practical yet often overlooked: the security and cleanliness of the environment. LocalStack feels exactly like real AWS, which makes the "it's just an emulator" instinct lull us into carelessness. As a result, production data ends up in the emulator, secrets leak into logs, and state gets overwritten between developers. This episode lays out the rules so LocalStack stays comfortable to use without creating production risks.
The first and most important rule: never import production data into LocalStack. The analogy is like bringing your real wallet to a toy store — without realizing it, you're carrying valuable assets to a place they shouldn't be. Production data usually contains PII, tokens, or financial data. Once it enters a local machine, that data leaves the AWS account perimeter: a lost laptop carries the volume along, and repository snapshots can be uploaded to the wrong location.
Replace real data with synthetic data. For realistic shape without meaning, generate your own data:
import boto3
from faker import Faker
fake = Faker()
dynamodb = boto3.client("dynamodb", endpoint_url="http://localhost:4566")
for _ in range(100):
dynamodb.put_item(
TableName="users",
Item={"id": {"S": fake.uuid4()}, "email": {"S": fake.email()}},
)If a scenario truly needs a sample of the production data structure, take anonymized schemas and small examples — not the raw dump.
Even anonymized data can hide secrets. Before importing, scrub values that resemble credentials or tokens. Common patterns are AWS access keys (AKIA...) or long tokens like ghp_...:
sed -i 's/AKIA[0-9A-Z]\{16\}/AKIAFAKEFAKEFAKEFAKE/g' users.json
sed -i 's/ghp_[a-zA-Z0-9]\{36\}/ghp_FAKE_TOKEN_PLACEHOLDER/g' users.json
sed -i 's/Bearer [a-zA-Z0-9._-]\{20,\}/Bearer TOKEN_PLACEHOLDER/g' users.jsonThe rule of thumb: any value that would be dangerous if leaked must be replaced before touching the emulator. Production credentials must also never flow into logs or a LocalStack environment shared between developers.
LocalStack is essentially one state on one machine. If a team shares an instance, developer A deleting a table breaks developer B's work. The solution: one instance per developer with a different port. Example mapping:
| Developer | Port | Endpoint |
|---|---|---|
| A | 4566 | http://localhost:4566 |
| B | 4567 | http://localhost:4567 |
| C | 4568 | http://localhost:4568 |
Developer B runs their own instance on port 4567:
docker run -d --name localstack-dev-b \
-p 4567:4566 -p 4510-4559:4510-4559 \
-e SERVICES=s3,dynamodb,sqs \
localstack/localstack:stableThen point the client at that port:
export AWS_ENDPOINT_URL=http://localhost:4567
awslocal s3 mb s3://isolated-bucketOne important note: even with different ports, the default data volume can be shared if not configured. Make sure each instance uses its own volume or VOLUME_DIR for true isolation.
Tests need determinism: start empty, seed the same way, then tear down. If dev state gets mixed in, tests become flaky — passing sometimes, failing sometimes depending on leftover data. The simplest way: two Compose profiles, dev uses PERSISTENCE, test doesn't:
services:
localstack:
image: localstack/localstack:stable
ports:
- "4566:4566"
environment:
PERSISTENCE: 0
DEBUG: 1Run it only for the test session with docker compose -f docker-compose.test.yml up -d. When done, the instance is discarded and state returns to empty for the next run.
PERSISTENCE=1 keeps state across restarts — very convenient for dev, very dangerous for tests. The common problems: stale data gets stored so configuration isn't reproducible, or sensitive state lingers in the volume. Here's a concise guide:
| Environment | PERSISTENCE | Reason |
|---|---|---|
| Local dev | 1 | Resume work without reset |
| Test suite | 0 | Clean state per run |
| CI | 0 | Reproducible from scratch |
| Demo/workshop | 1 + named volume | Can be rolled back |
For dev, separate the volume per project so nothing overwrites each other:
docker run -d --name localstack-dev \
-v localstack_dev_data:/var/lib/localstack \
-e PERSISTENCE=1 \
-p 4566:4566 localstack/localstack:stableDon't copy the default test/test pair straight into CI — replace it with unique values so they're easy to trace if they appear in logs. Example:
export AWS_ACCESS_KEY_ID="localstack-ci-$(uuidgen)"
export AWS_SECRET_ACCESS_KEY="localstack-ci-secret"The reasoning: if production credentials ever leak into tests (for example an env var overriding a value), this unique identity makes the leak immediately visible in logs — instead of silently passing under the test/test pair. Different values per job also help trace which job behaves strangely.
Summary of LocalStack security principles:
PERSISTENCE on) from test state (empty per run).With a solid security foundation, we can move on to bigger scenarios. In the next episode 16 we discuss Advanced Services: ECS and EKS up to Karpenter, Step Functions with HTTP Tasks, Kinesis, MSK, OpenSearch, RDS, Redshift, and Athena. See you there!