DEV Community

Gokul Suresh
Gokul Suresh

Posted on

Testing AWS-integrated apps for free: a local setup with Floci and ngrok

Build, test, and share AWS-powered applications without an AWS account or deployment.

The problem I kept running into

If your app talks to AWS — uploading files to S3, queuing jobs through SQS, running functions on Lambda — development normally forces an uncomfortable choice.

Option one: write separate "fake" code paths just for local development (if (isDev) return fakeData). This means the code path you're actually testing every day is not the code path that runs in production. Bugs in your real AWS integration logic slip through, because you never actually exercised it locally.

Option two: use a real AWS account for development too. This works, but it costs money for every test run, requires every teammate to have their own AWS credentials, and means someone has to manage IAM permissions just so people can build features.

Neither is great. There's a third option that solved this for me completely, and it costs nothing: run a local AWS look-alike on your own laptop, and use a tunneling tool to let others access what's running there. This post walks through exactly how, and why each piece exists.

The two tools, and what they actually do

Floci — a local AWS emulator

Floci is an open-source program that emulates AWS's API surface on your own machine. It listens on localhost:4566 and responds to the same requests real AWS would — create a bucket, upload a file, invoke a function — using the exact same request format Amazon defined.

The key detail: your application code doesn't know the difference. If you're using the official AWS SDK (@aws-sdk/client-s3, boto3, whatever your language's equivalent is), that code has no idea whether it's really talking to Amazon or to a program pretending to be Amazon. It just sends requests to whatever address it's configured with.

For some services, Floci goes further than a shallow mock — it spins up real backing containers. Point it at "RDS" and it actually runs Postgres. Point it at "ElastiCache" and it actually runs Redis. So you're not just getting plausible-looking fake responses; for the stateful services, you're getting the real thing, just running locally instead of in Amazon's data centers.

ngrok — a tunnel to your laptop

Once your app is running locally, it's still only reachable from your own machine — localhost means "this same computer," and your home or office router blocks incoming connections from strangers by default (a security feature, not a bug).

ngrok solves this with a trick worth understanding, because it explains why no firewall configuration is ever needed: your laptop opens an outbound connection to ngrok's servers (always allowed, since outbound traffic isn't blocked). That connection stays open, like a phone call that never hangs up. ngrok then hands you a public URL. When someone visits it, ngrok's servers receive the request and relay it back down the open connection to your laptop, which replies, and the reply relays back out the same way.

Nobody ever connects to your laptop directly. They connect to ngrok, and ngrok relays through the tunnel your laptop itself opened.

Request flow diagram

Simplified down to just the request/response round trip, here's what happens every time someone opens your ngrok link:

Browser
   │  1. GET https://xyz.ngrok.app
   ▼
ngrok edge server (public IP)
   │  2. forwarded down the open tunnel
   ▼
ngrok agent (your laptop)
   │  3. localhost:3000
   ▼
Your app handles the request
   │  4. response travels back up the same path
   ▼
Browser renders the page
Enter fullscreen mode Exit fullscreen mode

The full picture

Visitor's browser
        │
ngrok's public server (has a real public IP)
        │
Open tunnel (started BY your laptop, stays open)
        │
ngrok agent running on your laptop
        │
Your app (e.g. localhost:3000)
        │
AWS SDK calls -> Floci (localhost:4566)
        │
Real Docker containers for stateful services
        │
Data persisted to a folder on your own disk
Enter fullscreen mode Exit fullscreen mode

Three independent layers: your application code (unchanged), an emulation layer intercepting AWS calls, and an access layer exposing the whole thing publicly if you want to share it.

Architecture diagram

A cleaner, boxed view of the same setup — useful as a quick visual reference for the stack running on your machine:

                Internet
                    │
                    ▼
          ngrok Public URL
                    │
        Secure Tunnel (HTTPS)
                    │
                    ▼
┌──────────────────────────┐
│      Local Machine        │
│                           │
│  React / Next.js Frontend │
│            │              │
│  Express / Nest Backend   │
│            │              │
│         AWS SDK           │
│            │              │
│          Floci            │
│            │              │
│  S3 • DynamoDB • SQS      │
│  Lambda • Secrets Manager │
└──────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

Development workflow

Laid out as a loop, this is the day-to-day cycle the setup is built around:

Write Feature
      │
      ▼
Run Application Locally
      │
      ▼
Floci emulates AWS
      │
      ▼
Test Uploads
      │
      ▼
Share using ngrok
      │
      ▼
Receive Feedback
      │
      ▼
Deploy to Render
Enter fullscreen mode Exit fullscreen mode

Setting it up, step by step

1. Install Docker and ngrok

Floci runs inside a Docker container, so Docker needs to be installed and running first. ngrok is a separate download from ngrok.com.

Verify Docker is working:

docker info
Enter fullscreen mode Exit fullscreen mode

Register your ngrok account once:

ngrok config add-authtoken <your-token>
Enter fullscreen mode Exit fullscreen mode

2. Write a docker-compose.yml

This file tells Docker which image to run and how to configure it:

services:
  floci:
    image: floci/floci:1.5.11
    container_name: local_aws_cloud
    ports:
      - "4566:4566"
    environment:
      - FLOCI_STORAGE_MODE=persistent
      - SERVICES=s3,dynamodb,sqs,lambda,rds,secretsmanager
    volumes:
      - ./data:/app/data
      - /var/run/docker.sock:/var/run/docker.sock
Enter fullscreen mode Exit fullscreen mode

A few details worth explaining rather than just copying:

  • Pin a specific version tag (1.5.11, not :latest). If everyone on a team pulls :latest, people end up on different versions with different env var names, and setups silently drift apart.
  • ./data:/app/data is a bind mount — it maps a folder on your real hard disk to a folder inside the container. This is what makes storage survive restarts; without it, everything Floci stores would vanish the moment the container stopped.
  • The Docker socket mount (/var/run/docker.sock) is what lets Floci spin up real containers for services like RDS and Lambda, instead of faking them. It's also a real security boundary worth knowing about — more on that below.

3. Start it and confirm it's healthy

docker compose up -d
curl http://localhost:4566/_localstack/health
Enter fullscreen mode Exit fullscreen mode

The health check returns a JSON list of every AWS service Floci is currently emulating and whether each is running. If this doesn't come back, nothing downstream will work, so it's worth checking before moving on.

4. Point your app's AWS SDK at it

Set these before running your app:

AWS_ENDPOINT_URL=http://localhost:4566
AWS_ACCESS_KEY_ID=111111111111
AWS_SECRET_ACCESS_KEY=test
AWS_DEFAULT_REGION=us-east-1
Enter fullscreen mode Exit fullscreen mode

Floci accepts any credential value — it doesn't validate them the way real AWS does. The one exception: a 12-digit numeric access key gets treated as an account identifier, useful if multiple people share one Floci instance and want isolated resources. Anything else falls into a shared default account.

If your code constructs its own S3Client (or equivalent), it needs to actually read this endpoint variable — the SDK doesn't automatically know to redirect just because the variable exists, unless your code passes it through explicitly:

const s3 = new S3Client({
  credentials: { accessKeyId: process.env.AWS_ACCESS_KEY_ID, secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY },
  region: process.env.AWS_REGION || 'us-east-1',
  endpoint: process.env.AWS_ENDPOINT_URL || undefined,
  forcePathStyle: !!process.env.AWS_ENDPOINT_URL,
});
Enter fullscreen mode Exit fullscreen mode

The forcePathStyle flag matters and is easy to miss. Real AWS S3 prefers requests shaped like bucket-name.s3.amazonaws.com. Local emulators like Floci expect the older path style instead: localhost:4566/bucket-name. Without this flag, requests can fail even with the endpoint correctly configured — a genuinely confusing failure mode if you don't know to look for it.

5. Create a bucket

S3 requires a bucket to exist before anything can be uploaded into it — this rule holds on Floci exactly as it does on real AWS:

aws --endpoint-url http://localhost:4566 s3 mb s3://your-app-files
Enter fullscreen mode Exit fullscreen mode

The name has to match whatever your app's config expects, or uploads will fail with a "bucket does not exist" error that has nothing to do with your actual code.

6. Prove it end to end

Run your app, trigger a real upload through the actual UI, then check that the file landed:

aws --endpoint-url http://localhost:4566 s3 ls s3://your-app-files/ --recursive
Enter fullscreen mode Exit fullscreen mode

When I did this on a real project, a genuine uploaded file showed up immediately — same upload code path production will use, running entirely for free on a laptop. Worth noting: only the raw file bytes live here. Structured data — filenames, uploader, timestamps — typically stays in your actual database (Mongo, Postgres, whatever you're using), completely separate from Floci. Databases hold records and pointers; S3-style storage holds the heavy binary data. That split doesn't change for local dev — you've just relocated where the storage physically lives.

7. Share it, if you want to

ngrok http 3000
Enter fullscreen mode Exit fullscreen mode

Tunnel only your app's port. Never tunnel the Floci port (4566) directly. If someone reaches that port — through a second tunnel, a misconfigured proxy, or a CORS mistake — they get unauthenticated access to every emulated AWS service on your laptop, including the ones backed by real Docker containers with real host access.

Security considerations worth actually reading

  • The Docker socket mount is a real trust boundary. Floci having access to /var/run/docker.sock means a compromised emulator has host Docker control. Don't run this setup with that mount on a machine you'd consider sensitive.
  • Treat anything uploaded through the tunnel as landing in plaintext on your disk. Don't use this setup for real customer data — it's a development and testing tool by design, not a secure storage system.
  • For anything beyond a same-day test, use ngrok's access controls (basic auth or an IP allowlist) in front of the tunnel. Free tier doesn't include this, so a bare link should be treated as something anyone with it can open.
  • Your app's own authentication still applies. A tunnel just gets someone to your login screen — it doesn't bypass whatever session/auth logic your app already enforces.

ngrok's free tier, in actual numbers

As of ngrok's 2026 pricing, the free plan gives you: up to 3 concurrent endpoints, 1GB of data transfer per month, and 20,000 HTTP requests per month — not per day, per month, shared across everyone who visits. There's no hard session timeout — an endpoint can stay online continuously as a background service. First-time visitors do see an interstitial warning page before reaching your app (a 7-day cookie skips it after their first visit), and the public URL rotates on every restart unless you're on a paid plan with a reserved domain.

One clarification worth making: "3 concurrent endpoints" means 3 separate tunnels you can run at once — it has nothing to do with how many people can visit one tunnel's link simultaneously. Any number of people can open the same shared link at the same time; the actual ceiling is the monthly bandwidth and request totals, not concurrent visitor count.

Where this fits, and where it doesn't

This setup is not a replacement for real hosting. It's genuinely good for exactly two things: catching integration bugs against AWS-shaped services before they ever reach a real AWS bill, and sharing a live, working demo with someone in under a minute without deploying anything.

It's genuinely not good for anything resembling production traffic. Your laptop has to stay on, the free tier caps are real, and there's no uptime guarantee if your machine sleeps or your network drops.

For actual hosting — real users, real uptime — platforms like Vercel or Render are the right tool, and they solve a different problem entirely. One detail worth knowing if your app uses WebSockets (Socket.io, for example): Vercel's serverless functions don't support long-lived persistent connections, so a platform like Render, which runs actual persistent servers, is often the better fit for anything beyond simple HTTP APIs.

Floci + ngrok vs. Render / Vercel

Side by side, the two solve different problems — here's the quick version:

Feature Floci + ngrok Render / Vercel
Purpose Development Production
AWS Cost Free Real AWS
Deploy Required No Yes
Share Demo Yes Yes
Always Online No Yes
Laptop Required Yes No

The realistic workflow is to use both, at different stages: local Floci + ngrok while actively building and testing, and a real host once a feature is stable enough for real users to depend on it.

Top comments (0)