I needed a local AWS environment — a sandbox to poke at services without the pain of a properly configured account. For a few weeks that was Floci, an open-source drop-in replacement for LocalStack Community. The best free option I found — I wrote about it.
Then I wanted CloudFront in my local emulator. Floci doesn’t implement it. LocalStack ships CloudFront only in its paid tiers.
fakecloud (also free and open source) does implement CloudFront. And Route 53. And WAFv2. And Cognito. And Bedrock. 105 services, a single ~19 MB binary, no auth tokens, no account. Btw, it’s written in Rust :)
So I set up both, side by side: one repo, two Docker Compose profiles, one justfile. Switch engines per workload.
What we are building#
~/git-repos/local-aws/
├── docker-compose.yml # both services, gated by profiles
├── justfile # task runner, engine-parameterized
└── data/
├── floci/ # Floci persistent state
└── fakecloud/ # fakecloud persistent stateBoth engines bind localhost:4566, both accept test/test credentials. Only one runs at a time (port collision). Swap is just down && just up <engine>.
docker-compose.yml#
The Floci service lives in the same file behind its own profile. Here is the fakecloud half:
services:
fakecloud:
image: ghcr.io/faiscadev/fakecloud:latest
container_name: fakecloud
profiles: ["fakecloud"]
restart: unless-stopped
ports:
- "4566:4566"
environment:
FAKECLOUD_STORAGE_MODE: persistent
FAKECLOUD_DATA_PATH: /var/lib/fakecloud
FAKECLOUD_REGION: us-east-1
FAKECLOUD_ACCOUNT_ID: "000000000000"
FAKECLOUD_LOG: info
FAKECLOUD_ADDR: "0.0.0.0:4566"
volumes:
- ./data/fakecloud:/var/lib/fakecloud
- /var/run/docker.sock:/var/run/docker.sock
healthcheck:
test: ["CMD", "curl", "-fsS", "http://localhost:4566/_fakecloud/health"]
interval: 10s
timeout: 3s
retries: 5
start_period: 10sThree things to notice:
- Profiles. Each service sits behind a Compose profile of the same name.
docker compose upwith no profile starts nothing.docker compose --profile fakecloud up -dstarts only fakecloud. That keeps the engines mutually exclusive — no conditional includes. - The Docker socket is not decoration. fakecloud runs Lambda in real Docker containers with the official AWS runtime images.
RunInstancesboots a container per EC2 instance — user-data and all. Without the socket, EC2 degrades to metadata-only instances. - Health paths differ. Floci exposes
/_localstack/health(a deliberate LocalStack-compat alias). fakecloud uses/_fakecloud/healthand returns its own JSON shape:{"status":"ok","version":"0.41.1","services":[...]}.
justfile#
set shell := ["bash", "-cu"]
engine := env_var_or_default("AWS_EMU", "floci")
default:
@just --list
up engine=engine:
docker compose --profile {{engine}} pull
docker compose --profile {{engine}} up -d
@echo "{{engine}} at http://localhost:4566"
down:
docker compose --profile floci --profile fakecloud down
restart engine=engine: down (up engine)
logs engine=engine:
docker compose logs -f {{engine}}
health engine=engine:
@if [ "{{engine}}" = "floci" ]; then \
curl -fsS http://localhost:4566/_localstack/health | { command -v jq >/dev/null && jq . || cat; }; \
else \
curl -fsS http://localhost:4566/_fakecloud/health | { command -v jq >/dev/null && jq . || cat; }; \
fi
wipe engine=engine:
docker compose --profile {{engine}} down
rm -rf data/{{engine}}
@echo "{{engine}} state wiped."
AWS_EMU=fakecloud sticks the engine for the shell. Without it, just up defaults to Floci.
AWS CLI env#
~/.bash.d/aws.sh is sourced on every shell start:
# Local AWS emulator (Floci or fakecloud) at http://localhost:4566
# Switch engine: AWS_EMU=fakecloud just up (default: floci)
# Override per-shell when targeting real AWS (e.g. unset AWS_ENDPOINT_URL).
export AWS_ENDPOINT_URL=http://localhost:4566
export AWS_DEFAULT_REGION=us-east-1
export AWS_ACCESS_KEY_ID=test
export AWS_SECRET_ACCESS_KEY=testSame env shape works for both engines. Real AWS is one unset AWS_ENDPOINT_URL away.
Swap and smoke test#
With Floci running:
just down
just up fakecloud
just health fakecloudjust health fakecloud after a few seconds:
{
"status": "ok",
"version": "0.41.1",
"services": [
"s3", "cloudfront", "wafv2", "route53", "cognito-idp", "bedrock",
"lambda", "dynamodb", "sqs", "sns", "kms", "iam", "sts", ...
]
}105 services in the full list — CloudFront and WAFv2 included. Quick end-to-end:
aws s3 mb s3://tubely-private-13656
aws s3 cp ./video.mp4 s3://tubely-private-13656/
aws cloudfront list-distributionsAll return 200. To create a distribution with a WAFv2 ACL in monitor (count) mode:
aws wafv2 create-web-acl --region us-east-1 \
--name tubelycdn-monitor \
--scope CLOUDFRONT \
--default-action Allow={} \
--rules '[{"Name":"AWS-AWSManagedRulesCommonRuleSet","Priority":0,
"Statement":{"ManagedRuleGroupStatement":{"VendorName":"AWS","Name":"AWSManagedRulesCommonRuleSet"}},
"OverrideAction":{"Count":{}},
"VisibilityConfig":{"SampledRequestsEnabled":true,"CloudWatchMetricsEnabled":true,"MetricName":"common"}}]' \
--visibility-config SampledRequestsEnabled=true,CloudWatchMetricsEnabled=true,MetricName=tubelycdn
aws cloudfront create-distribution --distribution-config file://dist-tubelycdn.jsonYou get back a distribution ID, an ARN, and a *.cloudfront.net domain.
What fakecloud is good at#
APIs are clean and Smithy-perfect. Where behavior is simplified, the docs say so upfront: CloudFront’s data plane is a single local node; it forwards requests but doesn’t cache them. For my purposes that never mattered. Lambda and EC2 run in real containers — real execution.
fakecloud is also open to contributions, and reviews move fast. EC2 output was missing PlatformDetails, so I sent a PR. Reviewed and merged the same day. Very active and friendly maintainers.
When to reach for each#
| Need | Engine |
|---|---|
Tooling or CI that expects LocalStack conventions (/_localstack endpoints, drop-in swap) | Floci |
| CI fixture: deterministic API responses, plan/apply assertions, fast startup | fakecloud |
| CloudFront, Route 53, WAFv2, Cognito, Bedrock — services Floci skips | fakecloud |
| Docker-backed compute: Lambda, EC2, RDS, ElastiCache, ECS as real containers | either — both do it |
| Comparing two engines against the same code | both, swap via profile |
Wrap-up#
I started with Floci, and it earned its slot: drop-in LocalStack replacement, no license, no fuss. But fakecloud at 0.41 became my default engine. More services, real containers behind Lambda, EC2, RDS, and ElastiCache, plus CloudFront, Route 53, and WAFv2 that Floci skips. Floci keeps its Compose profile for tooling that expects LocalStack conventions. Two open-source emulators, one Docker Compose file, one localhost:4566 endpoint, over a hundred services between them. No paid licenses. No internet required.
References#
- fakecloud (and docs)
- Floci (and docs)
- just task runner
