Built by Paris | Floci + Terraform + Docker | May 2026
WrestleDream is a daily wrestling performance showdown app. The backend is a FastAPI Python service that fetches real match results, scores wrestlers, and returns matchup pairs. The frontend is React/Vite. The goal of this session was to deploy the entire stack locally using Floci as a local AWS emulator, with Terraform managing every cloud resource as code, all running inside Docker on a Mac Mini M-series.
| Layer | What it does |
|---|---|
| Docker | Runs Floci container — emulates all AWS services on port 4566 |
| Floci | Local AWS emulator — S3, Lambda, API Gateway, DynamoDB, SSM, IAM, CloudWatch |
| Terraform | Infrastructure as Code — provisions all 20 AWS resources against Floci |
| Lambda | Python 3.11 ARM64 — runs FastAPI via Mangum ASGI adapter |
| API Gateway | HTTP endpoint routing /api/* to Lambda |
| DynamoDB | Two tables — WrestlerCache and WrestleDreamDailyDeck |
| S3 | Hosts the React/Vite frontend build (static site) |
| SSM | Stores env vars (USE_SEED, USE_THESPORTSDB, etc.) |
| Vite Dev Server | Proxies /api/* to Floci API Gateway for local browser interaction |
Install all tools before starting. These only need to be done once.
| Tool | Install Command | Verify |
|---|---|---|
| Terraform | brew install terraform |
terraform -v |
| AWS CLI | brew install awscli |
aws --version |
| Docker | docker.com/get-started | docker -v |
| Python 3.11 | brew install python@3.11 |
python3 --version |
| Node 18+ | brew install node |
node -v |
You will use 3 terminal tabs throughout this session. Keep them organized exactly like this:
| Terminal | Purpose | What's Running |
|---|---|---|
| Terminal 1 | Floci daemon | docker run command — stays open the whole session |
| Terminal 2 | Main working terminal | Clone, scripts, terraform, npm, git commands |
| Terminal 3 | Verification | curl health checks and aws verify commands |
⚠️ Every command in this doc specifies which terminal to paste it into. Always follow the tab number.
Floci does not need real AWS credentials, but the AWS CLI still expects a profile to exist. Run this once.
In Terminal 2:
aws configure --profile floci
# AWS Access Key ID: test
# AWS Secret Access Key: test
# Default region name: us-east-1
# Default output format: jsonThis creates a ~/.aws/credentials entry with fake values. Floci accepts any credentials.
Floci runs as a Docker container. It emulates all AWS services on port 4566. Start it in Terminal 1 and leave it running for the entire session.
In Terminal 1 — leave this running:
docker run --rm -p 4566:4566 \
-v /var/run/docker.sock:/var/run/docker.sock \
floci/floci:latestIn Terminal 3 — verify Floci is up:
curl http://localhost:4566/_floci/health
# Expected: {"status":"running"}💡 The
--rmflag auto-removes the container when it stops so it won't pile up.
If Floci fails to start with a port binding error, an existing container may be holding port 4566.
In Terminal 2:
# See what's running
docker ps -a
# Force remove the conflicting container
docker rm -f <container_id>
# Then retry Phase 1In Terminal 2:
git clone https://github.com/Paristech1/WrestleDream
cd WrestleDreamIf the repo already exists locally you will see
fatal: destination path already exists. That's fine — justcdinto the existing folder.
The FastAPI backend must be zipped into a Lambda deployment package before Terraform can deploy it. This script handles everything.
In Terminal 2 — from WrestleDream root:
bash terraform/package_lambda.shExpected output: ✅ Done! ~8MB → terraform/lambda.zip
- Installs
requirements.txt+mangumusing--platform manylinux_2_17_aarch64(Linux ARM64 binary wheels) - Copies
backend/source files to the build folder - Copies
api/index.pyas the Lambda entry point - Appends
handler = Mangum(app, lifespan='off')so Lambda knows what to call - Zips everything into
terraform/lambda.zip - Verifies the
pydantic_core.sobinary is ELF ARM64 — NOT macOS Mach-O
Terraform reads main.tf and provisions all infrastructure against Floci. Run from inside the terraform/ directory.
In Terminal 2:
cd terraform
terraform init
terraform plan
terraform apply -auto-approveterraform init— downloads the AWS provider plugin (one-time)terraform plan— dry run, shows all ~20 resources to be createdterraform apply -auto-approve— deploys everything to Floci in under 30 seconds
| Resource | Name | Purpose |
|---|---|---|
| S3 Bucket | wrestledream-frontend |
Hosts the React build |
| DynamoDB Table | WrestlerCache |
Caches wrestler images and scores |
| DynamoDB Table | WrestleDreamDailyDeck |
Caches daily deck results |
| SSM Parameters | /wrestledream/* |
USE_SEED, USE_THESPORTSDB, CACHE_TTL_SECONDS, ALLOWED_ORIGINS |
| IAM Role | wrestledream-lambda-role |
Lambda execution permissions |
| Lambda Function | wrestledream-api |
Python 3.11 ARM64 FastAPI backend |
| API Gateway | wrestledream-api |
HTTP /api/* routing to Lambda |
| API Gateway Stage | dev |
Live URL for all API calls |
| CloudWatch Log Group | /aws/lambda/wrestledream-api |
Lambda logs, 7-day retention |
In Terminal 2:
cd ~/WrestleDream/WrestleDream/frontend
npm install
npm run buildThen upload the build to Floci S3:
In Terminal 2:
aws --endpoint-url=http://localhost:4566 \
s3 sync dist/ s3://wrestledream-frontend \
--profile flociExpected: 4 files uploaded — index.html, assets/index.css, assets/index.js, vite.svg
In Terminal 3:
aws --endpoint-url=http://localhost:4566 s3 ls --profile floci
aws --endpoint-url=http://localhost:4566 dynamodb list-tables --profile floci
aws --endpoint-url=http://localhost:4566 lambda list-functions --profile flociIn Terminal 3:
aws --endpoint-url=http://localhost:4566 lambda invoke \
--function-name wrestledream-api \
--profile floci \
--cli-binary-format raw-in-base64-out \
--payload '{"version":"2.0","routeKey":"GET /api/health","rawPath":"/api/health","headers":{"content-type":"application/json"},"requestContext":{"http":{"method":"GET","path":"/api/health","protocol":"HTTP/1.1","sourceIp":"127.0.0.1","userAgent":"curl"},"stage":"dev"},"isBase64Encoded":false}' \
/tmp/health.json && cat /tmp/health.jsonExpected: {"statusCode":200,"body":"{\"status\":\"ok\",\"app\":\"WrestleDream\"}"}
⚠️ Must use API Gateway v2 payload format (version: "2.0") withsourceIpinrequestContext.http. The v1 format causes Mangum to fail.
In Terminal 3:
API_ID=$(aws --endpoint-url=http://localhost:4566 apigateway get-rest-apis \
--profile floci --query 'items[0].id' --output text)
curl "http://localhost:4566/restapis/$API_ID/dev/_user_request_/api/health"Expected: {"status":"ok","app":"WrestleDream"}
Floci doesn't add CORS headers, so the frontend can't be served directly from S3 in a browser. Instead, run Vite's dev server pointing at Floci — Vite's proxy handles routing and adds CORS headers automatically.
In Terminal 2:
cd ~/WrestleDream/WrestleDream/frontend
WRESTLEDREAM_API="http://localhost:4566/restapis/<api_id>/dev/_user_request_" \
npm run dev -- --config vite.config.dev.jsThen open http://localhost:5173 (or 5174 if 5173 is in use) in your browser.
Every /api/* call routes: Browser → Vite proxy → Floci API Gateway → Lambda → FastAPI
This is the core demo of Floci + Terraform: instant destroy and full redeploy in under 30 seconds.
In Terminal 2:
cd ~/WrestleDream/WrestleDream/terraform
terraform destroy -auto-approveIn Terminal 1:
# Press Ctrl+C to stop Floci- Start Floci again — Phase 1
- Run
bash terraform/package_lambda.shif code changed — Phase 3 - Run
terraform apply -auto-approve— all resources back in ~30 seconds
In Terminal 2 — from WrestleDream root:
echo "terraform/.terraform/" >> .gitignore
echo "terraform/lambda.zip" >> .gitignore
echo "terraform/terraform.tfstate*" >> .gitignore
echo "terraform/.lambda_build/" >> .gitignore
git add terraform/main.tf terraform/variables.tf \
terraform/outputs.tf terraform/package_lambda.sh .gitignore
git commit -m "feat: add Terraform IaC + Floci local dev setup"
git pushNever commit
lambda.zip,.terraform/, ortfstate— these are build artifacts and contain local paths.
These were real blockers encountered during this session. Each one is documented so you can recognize and fix them fast.
What happened: Lambda invocation returned FunctionError: Handled with a 30-second wait every time.
Why: Two compounding bugs. First, Terraform's architectures field was missing so Lambda defaulted to x86_64. Floci on Mac M-series runs ARM64 — the binary mismatch caused pydantic_core to fail on import and the Lambda never started. Second, api/index.py had an unconditional sys.path.insert pointing to a backend/ subdirectory that doesn't exist inside the Lambda zip.
Fix: Added architectures = ["arm64"] to aws_lambda_function in main.tf. Fixed api/index.py to use a conditional os.path.isdir() check. Rebuilt the zip with --platform manylinux_2_17_aarch64.
What happened: pydantic_core._pydantic_core ImportModuleError — Lambda couldn't load the C extension.
Why: pip downloaded macOS ARM64 (darwin_arm64) wheels from its local cache because the old flag --platform manylinux2014_aarch64 is an alias that pip can mis-resolve. The .so file was a Mach-O binary instead of Linux ELF.
Fix: Run pip cache purge first, then use --platform manylinux_2_17_aarch64 (explicit new-style tag). The package_lambda.sh script verifies the .so with the file command — it must say ELF ARM aarch64, not Mach-O.
What happened: aws lambda invoke returned an error about base64 encoding on the --payload flag.
Why: Newer AWS CLI v2 requires --cli-binary-format raw-in-base64-out when passing a JSON string to --payload.
Fix: Always include --cli-binary-format raw-in-base64-out in every lambda invoke command.
What happened: Lambda responded with errorType: KeyError: 'sourceIp' when invoked through the CLI.
Why: Mangum v0.17+ requires the API Gateway v2 payload format (version: "2.0" with requestContext.http.sourceIp). The v1 format (httpMethod/path) causes Mangum to fail parsing the event.
Fix: Switch to the v2 payload format shown in Phase 6 test commands. Always include sourceIp and userAgent in requestContext.http.
What happened: terraform destroy errored on API Gateway 404 and only destroyed 9 of 20 resources. Subsequent terraform apply updated Lambda but never applied the arm64 architecture change.
Why: Floci wipes its in-memory resource state between restarts. When Terraform tried to destroy the API Gateway, Floci returned 404 because the resource was already gone. The partial destroy left Lambda in place with x86_64 architecture.
Fix: Bypass Terraform for the architecture update using AWS CLI directly:
aws --endpoint-url=http://localhost:4566 lambda update-function-code \
--function-name wrestledream-api --profile floci \
--zip-file fileb:///Users/me/WrestleDream/WrestleDream/terraform/lambda.zip \
--architectures arm64What happened: curl to the API Gateway URL returned {"message":"Stage not found"} even after terraform apply succeeded.
Why: The aws_api_gateway_deployment resource used the deprecated stage_name attribute. When Floci's state was wiped between sessions, the stage disappeared.
Fix: Replaced the deprecated stage_name on the deployment resource with a separate aws_api_gateway_stage resource:
resource "aws_api_gateway_stage" "dev" {
rest_api_id = aws_api_gateway_rest_api.main.id
deployment_id = aws_api_gateway_deployment.main.id
stage_name = "dev"
}Added a triggers block to force redeployment when the integration changes. Added lifecycle { create_before_destroy = true }.
What happened: Opening the S3 URL in a browser showed a white page. The React app loaded but API calls failed.
Why: The frontend's DAILY_DECK_URL was set to the relative path /api/daily-deck. When served directly from S3, there was no proxy to route that to the API Gateway URL. Floci also does not add CORS headers to API Gateway responses.
Fix: Run the frontend on Vite's dev server with WRESTLEDREAM_API pointing at Floci. The devApiPlugin.js proxy handles routing and adds Access-Control-Allow-Origin: *. Full stack: Browser → Vite proxy → Floci API Gateway → Lambda → FastAPI.
What happened: Docker failed to start Floci because port 4566 was already bound.
Why: A previous Floci container was still running or stopped but not removed.
Fix:
docker ps -a # find the conflicting container
docker rm -f <id> # force remove it
# then retry Phase 1Mac M1/M2/M3/M4 chips are ARM64. When pip installs packages natively on your Mac, it downloads darwin_arm64 binaries. Lambda runs on Linux. If you package those native binaries into the Lambda zip, they crash immediately because Linux cannot execute macOS Mach-O binaries. You must cross-compile using pip's --platform flag to get the correct Linux ELF binaries. Floci on Mac M-series runs Lambda in ARM64 mode, so the correct target is manylinux_2_17_aarch64.
Floci stores all resource state in memory, not on disk. Every time the Docker container stops, everything is wiped. This is why Terraform may show resources as existing in its .tfstate but Floci returns 404 when trying to delete them. Always run terraform apply after restarting Floci to reconcile state. This behavior is also what makes Floci perfect for rapid teardown/redeploy demos.
Lambda expects a specific event format and a synchronous handler function. FastAPI uses ASGI, which is asynchronous and expects a different calling convention. Mangum is the adapter that sits between them — it translates an incoming Lambda event into an ASGI scope, runs the FastAPI app, and translates the response back into the Lambda format.
Every resource in this project was defined in main.tf and variables.tf. This means the entire infrastructure is version-controlled, reproducible, and can be torn down or redeployed in seconds. Your cloud environment becomes code you can commit, review, and share — rather than manual clicks in a console that nobody can audit or reproduce.
# Terminal 1 — Start Floci
docker run --rm -p 4566:4566 -v /var/run/docker.sock:/var/run/docker.sock floci/floci:latest
# Terminal 3 — Verify
curl http://localhost:4566/_floci/health
# Terminal 2 — Deploy
cd ~/WrestleDream/WrestleDream/terraform && terraform apply -auto-approve# Terminal 2
cd ~/WrestleDream/WrestleDream
pip cache purge
bash terraform/package_lambda.sh
cd terraform && terraform apply -auto-approve# Terminal 2
cd ~/WrestleDream/WrestleDream/frontend
WRESTLEDREAM_API="http://localhost:4566/restapis/<api_id>/dev/_user_request_" \
npm run dev -- --config vite.config.dev.js
# Open: http://localhost:5173# Terminal 2
cd ~/WrestleDream/WrestleDream/terraform && terraform destroy -auto-approve
# Terminal 1: Ctrl+COnly two changes needed in terraform/main.tf:
- Remove the
endpoints {}block from the provider - Remove the fake
access_key/secret_keylines - Set your real profile:
export AWS_PROFILE=your-real-profile - Run
terraform apply— identical infrastructure, real AWS
The Lambda zip packaging, Terraform resources, and all commands remain exactly the same. Floci is a drop-in local replacement for real AWS.
Built by Paris | Floci + Terraform + Docker | May 2026