Testcontainers vs LocalStack vs Docker Compose: Which for Local AWS Dev in 2026?
Testcontainers, LocalStack, and plain Docker Compose compared for local development and integration testing against AWS-like services in 2026 — fidelity to real AWS behavior, test integration, and which fits your workflow.
Testing code that talks to S3, DynamoDB, or SQS without hitting real AWS (cost, speed, and blast radius reasons) has three common approaches, and they solve different parts of the problem. Here is an honest comparison.
Quick Comparison
| Testcontainers | LocalStack | Docker Compose (manual) | |
|---|---|---|---|
| Purpose | Programmatic container lifecycle for tests | AWS service emulation | General-purpose local service orchestration |
| AWS service coverage | N/A — brings up real services (Postgres, Kafka, etc.), not AWS emulation | Broad (S3, DynamoDB, SQS, Lambda, 80+ services) | N/A — you wire up whatever images you choose |
| Fidelity to real behavior | High — it's the real service (real Postgres, real Kafka) | Good but not perfect emulation, edge cases diverge | Depends entirely on what you run |
| Test integration | Native — designed specifically for test lifecycle | Manual, or via testcontainers-localstack module | Manual, no test-specific tooling |
| Best fit | Integration tests needing real dependent services | Local dev/testing against AWS SDK calls specifically | Local dev environments, not primarily testing |
Testcontainers
Testcontainers is a library (Java, Node, Python, Go, etc.) that spins up real, throwaway Docker containers for your test's actual dependencies — a real Postgres, a real Kafka, a real Redis — programmatically, scoped to the test lifecycle.
from testcontainers.postgres import PostgresContainer
import pytest
@pytest.fixture(scope="module")
def postgres_container():
with PostgresContainer("postgres:16") as postgres:
yield postgres
def test_user_repository(postgres_container):
conn_url = postgres_container.get_connection_url()
repo = UserRepository(conn_url)
repo.create_user("test@example.com")
assert repo.find_by_email("test@example.com") is not NoneTestcontainers strengths:
- Maximum fidelity — it's the real Postgres/Kafka/Redis binary, not an emulation, so behavior differences that bite you in production won't hide in tests
- Deep language-specific test framework integration — container lifecycle ties directly to test setup/teardown
- Broad module ecosystem beyond AWS — databases, message queues, even Selenium/browser containers
Testcontainers weaknesses:
- Doesn't directly solve "emulate AWS SDK calls" — the LocalStack module exists but AWS-specific emulation isn't its core strength
- Test suite startup time grows with container count if not managed carefully (shared containers across test classes help)
- Requires Docker running wherever tests execute, including CI runners
When to use Testcontainers: Your tests need a real database, message queue, or other real service dependency with production-fidelity behavior — not specifically AWS API emulation.
LocalStack
LocalStack emulates the actual AWS API surface locally — your code makes real AWS SDK calls against localhost:4566 instead of real AWS, with no code changes beyond the endpoint.
import boto3
s3 = boto3.client(
"s3",
endpoint_url="http://localhost:4566", # Only difference from real AWS
aws_access_key_id="test",
aws_secret_access_key="test",
)
s3.create_bucket(Bucket="test-bucket")
s3.put_object(Bucket="test-bucket", Key="file.txt", Body=b"content")docker run -d -p 4566:4566 localstack/localstackLocalStack strengths:
- Purpose-built specifically for AWS SDK emulation — the broadest AWS service coverage of any local emulation tool (80+ services)
- Genuinely useful for local dev without an AWS account/costs, not just testing
- Pro tier adds even deeper fidelity (IAM enforcement, more complete Lambda runtime emulation) for teams that need it
LocalStack weaknesses:
- Emulation, not the real thing — edge cases and less-common API behaviors sometimes diverge from real AWS, which can mask bugs that only show up against real S3/DynamoDB
- Free tier service coverage has gaps compared to Pro — some services need the paid tier for full fidelity
- Adds a dependency on LocalStack's own emulation quality being correct, on top of your own code
When to use LocalStack: Your code specifically calls AWS SDK APIs (S3, DynamoDB, SQS, Lambda, etc.) and you want to develop/test against that without real AWS costs or credentials.
Docker Compose (Manual Setup)
Just running the actual services you need via docker-compose.yml, without a dedicated framework — the simplest and most transparent option, at the cost of doing more wiring yourself.
# docker-compose.test.yml
services:
postgres:
image: postgres:16
environment:
POSTGRES_PASSWORD: test
ports:
- "5432:5432"
redis:
image: redis:7
ports:
- "6379:6379"Docker Compose strengths:
- Full transparency and control — no framework abstraction, you see exactly what's running and why
- Works identically for local dev and CI with the same file
- No new tool to learn if your team already knows Compose
Docker Compose weaknesses:
- No test-lifecycle integration — you manage startup/teardown/health-checks yourself, which Testcontainers does natively
- No AWS-specific emulation — you'd still need LocalStack as one of the services in your Compose file for that
- Manual health-check wiring means flaky "service not ready yet" failures are more common without careful scripting
When to use Docker Compose: Simple local dev environments, or as the underlying mechanism you'd wire LocalStack or other services into without a dedicated test framework layer on top.
The Honest Verdict
Testing against real database/queue behavior with production fidelity: Testcontainers. Nothing beats testing against the actual service binary.
Your code specifically talks to AWS SDK APIs and you want that emulated locally: LocalStack. Purpose-built for exactly this, broadest AWS coverage available.
Simple local dev environment, no dedicated test framework needed: Docker Compose. Full control, works everywhere, but you own more of the wiring.
They compose well together — Testcontainers has a LocalStack module specifically so you can get Testcontainers' lifecycle management and LocalStack's AWS emulation in the same test suite, which is increasingly the common pattern for teams testing AWS-heavy applications.
More local dev tooling comparisons? Read our Docker Compose complete guide and Render vs Railway vs Fly.io PaaS comparison.
Today I Fixed
Short real fixes from production — posted daily
Stay ahead of the curve
Get the latest DevOps, Kubernetes, AWS, and AI/ML guides delivered straight to your inbox. No spam — just practical engineering content.
Related Articles
ArgoCD vs Spinnaker vs Flux: GitOps Continuous Delivery Comparison 2026
ArgoCD, Spinnaker, and Flux CD compared for Kubernetes continuous delivery in 2026 — GitOps approach, multi-cluster support, canary/blue-green deployments, UI, RBAC, and which fits startups vs enterprises.
AWS ECR Push Denied: no basic auth credentials — Fix
Getting 'no basic auth credentials' or 'denied: Your authorization token has expired' when pushing to AWS ECR? Here are the exact commands to fix authentication for Docker, GitHub Actions, and Kubernetes.
AWS ECR vs Docker Hub vs GitHub Container Registry: Which One Should You Use?
A practical comparison of AWS ECR, Docker Hub, and GitHub Container Registry (GHCR) for storing container images in 2026 — covering cost, security, pull limits, CI/CD integration, and when each makes sense.