Raised: $0
0% of monthly goal Help us cross the finish line!
Goal: $12,000
Raised: $0 Goal: $12,000
0% of monthly goal Help us cross the finish line!
Sponsor DDEV

If you find this add-on useful, please star it on GitHub — stars show appreciation and help maintainers know their work matters.

ddev-floci-aws

add-on registry tests last commit release

A DDEV add-on that runs Floci — a free, open-source local AWS emulator — alongside your project, and points your web container at it.

Your application talks to http://floci-aws:4566 instead of AWS. S3 buckets, DynamoDB tables, SQS queues, SNS topics, Secrets Manager secrets, Lambda functions and about seventy other services exist locally, with no AWS account, no auth token, and no feature gates.

Why this add-on

LocalStack’s community edition sunset in March 2026: it now requires an auth token and its security updates are frozen. Floci is a drop-in replacement under the MIT license — same port, same credentials model, same SDK configuration — and it is meaningfully lighter, starting in milliseconds rather than seconds.

Running it as a DDEV add-on rather than as a separate docker compose stack buys you three things that are fiddly to arrange by hand:

Running alongside the Azure and GCP emulators

Everything this add-on installs is named for the cloud it emulates rather than for Floci: the service and its hostname are floci-aws, the container is ddev-<project>-floci-aws, the state volume is floci-aws, and the command is ddev floci-aws. Nothing here claims the bare floci name, so the equivalent Azure and GCP add-ons can be installed in the same project without overwriting this one’s files or shadowing its command.

Your application can then tell them apart by endpoint: http://floci-aws:4566, http://floci-az:4577, http://floci-gcp:4588.

Installing it in several projects at once

State is per project, not shared. The floci-aws volume is a Compose named volume, so Docker namespaces it by Compose project — DDEV gives each project ddev-<project>_floci-aws. Two projects each get their own buckets, tables and queues, ddev floci-aws reset in one leaves the other untouched, and deleting one project’s volume cannot corrupt another’s. Both projects can serve on router port 4567 at once, because the router routes by hostname.

The exception is Docker-backed services, which are not namespaced upstream. Floci names some of the containers it starts globally — the ECR registry is floci-ecr-registry, with no project in the name — so the first project to ask for one gets it, and a second project asking for the same service silently reuses that container even though it is attached to the first project’s network and therefore unreachable. If two projects both need ECR, RDS or another Docker-backed service, run them one at a time, or keep the Docker-backed work in a single project. Everything in-process (S3, DynamoDB, SQS, SNS, IAM, Secrets Manager, …) is unaffected.

Installation

ddev add-on get codementality/ddev-floci-aws
ddev restart

Then check it:

ddev floci-aws aws s3 mb s3://uploads
ddev floci-aws aws s3 ls

Using it

Nothing in your application needs to know it is talking to an emulator. The web container already has the environment set:

Variable Value
AWS_ENDPOINT_URL, AWS_ENDPOINT http://floci-aws:4566
AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY test / test
AWS_DEFAULT_REGION, AWS_REGION us-east-1
AWS_USE_PATH_STYLE_ENDPOINT, AWS_S3_FORCE_PATH_STYLE true
AWS_BUCKET, AWS_PUBLIC_BUCKET public
AWS_PRIVATE_BUCKET private
AWS_PUBLIC_BUCKET_URL https://<project>.ddev.site:4567/public — browser-facing

PHP (aws/aws-sdk-php 3.300+ reads AWS_ENDPOINT_URL on its own):

$s3 = new Aws\S3\S3Client([
  'version' => 'latest',
  'region'  => getenv('AWS_DEFAULT_REGION'),
  // Older SDK versions need these two spelled out:
  'endpoint' => getenv('AWS_ENDPOINT_URL'),
  'use_path_style_endpoint' => true,
]);
$s3->putObject(['Bucket' => 'uploads', 'Key' => 'a.txt', 'Body' => 'hi']);

For Drupal with s3fs or flysystem modules, point the bucket at uploads and the custom host at floci-aws:4566 with path-style addressing on.

Node / Python / Go: the v3 JS clients, botocore ≥ 1.31 and the Go v2 SDK all honour AWS_ENDPOINT_URL with no configuration at all.

Terraform / OpenTofu / CDK work too — point the provider’s endpoints block at the same URL.

Reaching Floci from the host

There are two endpoints, and they are not interchangeable:

ddev floci-aws url
# From the project (web container, Lambda, RDS): http://floci-aws:4566
# From the host (aws CLI, Terraform, your IDE):   https://<project>.ddev.site:4567

The in-project URL is the one baked into service URLs Floci returns, because that is what your application code has to be able to reach. A host-side AWS CLI pointed at https://<project>.ddev.site:4567 will therefore get back queue URLs naming floci-aws:4566, which the host cannot resolve. That is a real limitation, not an oversight — you cannot have both without a hostname that resolves in both places.

If your workflow is host-first (running Terraform from your terminal rather than from ddev ssh), flip it:

ddev dotenv set .ddev/.env.floci-aws \
  --floci-aws-base-url=https://myproject.ddev.site:4567
ddev restart

The simplest way to avoid the question entirely is ddev floci-aws aws ..., which runs the CLI inside the container where both halves agree.

The two default S3 buckets

Out of the box the add-on creates two buckets on every start, with genuinely different permissions:

Bucket Default name Unsigned read Unsigned write Unsigned list
Public public ✅ objects readable ❌ 403 ❌ 403
Private private ❌ 403 ❌ 403 ❌ 403

The public bucket is for the 90% case — images, document uploads, anything meant to be served straight to a browser once it lands. It gets a bucket policy granting s3:GetObject to *, so an object is readable the moment it is written and your application never has to remember to set public-read on each upload. Writing still requires credentials.

Listing is deliberately not public by default. A publicly listable bucket is a misconfiguration rather than a feature, and nothing needs it to serve an <img src>.

This does not affect application code. Only anonymous listing is blocked. A signed ListObjectsV2 — what the AWS SDK inside Drupal issues, including the prefix/delimiter form that drives folder-style browsing in Media Library and s3fs — works on both buckets, because the web container already holds credentials. Only an unauthenticated browser hitting the bucket root gets a 403.

If you do find something that needs anonymous listing, it is a switch rather than a hook edit:

ddev dotenv set .ddev/.env.floci-aws --floci-aws-public-bucket-list=true
ddev restart

That adds s3:ListBucket on the bucket ARN to the policy. Unsigned writes stay refused either way.

The private bucket has no ACL and no policy — S3’s owner-only default. Nothing gets in without credentials or a presigned URL, which is what makes it useful for testing Flysystem’s presigned links and private uploads.

Measured against floci/floci:1.7.0 on 2026-08-26. Floci is under active development, so this may already be out of date.

The web container gets all of this in its environment:

Variable Value
AWS_PUBLIC_BUCKET public
AWS_PRIVATE_BUCKET private
AWS_BUCKET public — the 90% case, so config can stay short
AWS_PUBLIC_BUCKET_URL https://<project>.ddev.site:4567/public

AWS_PUBLIC_BUCKET_URL is the one worth knowing about. A URL built from AWS_ENDPOINT_URL names floci-aws:4566, which resolves only inside Docker — paste it into an <img src> and the browser shows a broken image. This variable is the same object as ddev-router serves it, which is what actually loads:

$src = getenv('AWS_PUBLIC_BUCKET_URL') . '/' . $key;   // renders
$src = getenv('AWS_ENDPOINT_URL') . '/public/' . $key; // broken image

Rename either bucket, or set one to an empty string to skip it:

ddev dotenv set .ddev/.env.floci-aws --floci-aws-public-bucket=assets
ddev dotenv set .ddev/.env.floci-aws --floci-aws-private-bucket=""
ddev restart

This depends on auth enforcement being on

FLOCI_AWS_S3_ENFORCE_AUTH defaults to true in this add-on — unlike upstream Floci and unlike LocalStack Community, which both default it off.

That is a deliberate difference, because with enforcement off S3 ignores signatures entirely: every bucket is world-readable and world-writable, the policy above is decorative, and the two bucket names imply a boundary that is not there. It also makes local behaviour diverge from real AWS in the direction that hides bugs — code that forgets to sign works locally and fails in production.

If something in your stack genuinely cannot sign requests, turn it off and accept that both buckets are then equally open:

ddev dotenv set .ddev/.env.floci-aws --floci-aws-s3-enforce-auth=false

The init hook prints a warning on every start while it is off, so this cannot be true by accident.

Presigned URLs

Presigned URLs work against the private bucket, and the signature is really checked — a tampered one gets a 403. The catch is the one from Reaching Floci from the host: the signature covers the Host header, so a URL is only valid at the endpoint it was signed for.

$s3 = new Aws\S3\S3Client([
  'version'  => 'latest',
  'region'   => getenv('AWS_DEFAULT_REGION'),
  // Not AWS_ENDPOINT_URL — the browser has to be able to resolve this host.
  'endpoint' => 'https://' . getenv('DDEV_HOSTNAME') . ':4567',
  'use_path_style_endpoint' => true,
]);
$cmd = $s3->getCommand('GetObject', [
  'Bucket' => getenv('AWS_PRIVATE_BUCKET'),
  'Key'    => $key,
]);
$url = (string) $s3->createPresignedRequest($cmd, '+20 minutes')->getUri();

Or flip it project-wide with --floci-aws-base-url=https://<project>.ddev.site:4567 if your whole workflow is host-first.

The buckets are created on start, not emptied: with the default hybrid storage mode their contents survive restarts. Renaming creates the new bucket and leaves the old one alone, so nothing is lost by changing a setting. Use ddev floci-aws reset to start over.

The hook itself is floci-aws/init/ready.d/10-default-buckets.sh, an ordinary init script with nothing special about it. Delete it if you would rather do your own thing, or read it as the worked example for the section below.

Creating resources on every start

Drop scripts in .ddev/floci-aws/init/ready.d/. They run once the AWS API is accepting requests, on every container start, and the aws CLI inside the container already points at the emulator:

#!/bin/sh
set -eu
aws s3api create-bucket --bucket uploads 2>/dev/null || true
aws sqs create-queue --queue-name jobs 2>/dev/null || true

chmod +x it and ddev restart. Make them idempotent — with the default storage mode the resources they create survive restarts, so a script that fails on “already exists” will break your next start. The shipped 10-default-buckets.sh shows the pattern: head-bucket first, create only if that misses, so a genuine failure is still reported instead of swallowed by || true.

There are four phases (boot.d, start.d, ready.d, stop.d); see floci-aws/init/README.md. .py scripts work as well as .sh.

Persistence

The default storage mode is hybrid: in-memory speed with an asynchronous flush every few seconds, so your buckets and tables survive ddev restart. State lives in a Docker volume, not in your project directory.

# Start clean.
ddev floci-aws reset

# Or make every start clean.
ddev dotenv set .ddev/.env.floci-aws --floci-aws-storage-mode=memory
ddev restart

Other modes are persistent (flush on every write) and wal (write-ahead log, maximum durability). Use memory in CI.

Docker-backed services and the Docker socket

Floci runs Lambda, RDS, Neptune, ElastiCache, MSK, ECS, EC2, EKS, OpenSearch, DocumentDB and CodeBuild as real containers rather than mocking their responses — that is what makes a Lambda actually execute your handler and an RDS instance actually speak Postgres. Doing so requires the host’s Docker socket, which docker-compose.floci-aws-docker.yaml mounts.

Mounting the Docker socket into a container gives it control of the host’s Docker daemon, which is effectively root on the host. It is the same bargain LocalStack and Testcontainers ask for, but it is worth making deliberately. If this project only uses the in-process services — S3, DynamoDB, SQS, SNS, IAM, KMS, Secrets Manager, SSM, EventBridge, Step Functions, API Gateway, Cognito, Kinesis, SES and the rest — you can decline it:

rm .ddev/docker-compose.floci-aws-docker.yaml
ddev restart

Everything in-process keeps working; the Docker-backed services do not start.

One wrinkle worth knowing: containers Floci starts are its own, not DDEV’s, so ddev stop leaves them running. ddev floci-aws reset removes the ones belonging to this project along with the state.

Migrating from LocalStack

LocalStack parity is on by default, so an existing setup mostly moves across unchanged:

Turn the translation off with ddev dotenv set .ddev/.env.floci-aws --floci-aws-localstack-parity=false.

Configuration

Every setting is a ddev dotenv set away, followed by ddev restart:

ddev dotenv set .ddev/.env.floci-aws --floci-aws-region=eu-west-1
Variable Default Purpose
FLOCI_AWS_IMAGE floci/floci:latest-compat Image tag. floci/floci:latest is the lean ~90 MB native build without the AWS CLI or boto3
FLOCI_AWS_HOSTNAME floci-aws Hostname on DDEV’s network, and the host in returned service URLs
FLOCI_AWS_BASE_URL http://floci-aws:4566 Base URL Floci uses when it builds service URLs
FLOCI_AWS_HTTP_PORT 4566 Router port (http)
FLOCI_AWS_HTTPS_PORT 4567 Router port (https)
FLOCI_AWS_REGION us-east-1 Default region
FLOCI_AWS_ACCOUNT_ID 000000000000 Default account id
FLOCI_AWS_STORAGE_MODE hybrid memory, persistent, hybrid or wal
FLOCI_AWS_DOCKER_NETWORK ddev-<project>_default Network for containers Floci starts
FLOCI_AWS_LOCALSTACK_PARITY true Accept LocalStack env vars and endpoints
FLOCI_AWS_S3_ENFORCE_AUTH true Enforce S3 public/private access and reject unsigned requests. On by default here, unlike upstream
FLOCI_AWS_ACCESS_KEY_ID test Credentials handed to the web container
FLOCI_AWS_SECRET_ACCESS_KEY test Credentials handed to the web container
FLOCI_AWS_S3_PATH_STYLE true Path-style S3 addressing in the web container
FLOCI_AWS_PUBLIC_BUCKET public Bucket created on every start with a public-read policy. Empty string to skip
FLOCI_AWS_PRIVATE_BUCKET private Owner-only bucket created on every start. Empty string to skip
FLOCI_AWS_PUBLIC_BUCKET_LIST false Allow anonymous listing of the public bucket. Signed listing works regardless

Floci reads any FLOCI_* variable directly; for anything not listed above — FLOCI_SERVICES_RDS_DEFAULT_POSTGRES_IMAGE, say — add it to the environment: block of .ddev/docker-compose.floci-aws.yaml, or override it in a compose file of your own. See the upstream configuration reference.

Multi-account isolation works out of the box: set AWS_ACCESS_KEY_ID to exactly twelve digits and Floci treats it as the account id, so resources from one account are invisible to another.

The ddev floci-aws command

Command Does
ddev floci-aws aws <args> Runs the AWS CLI inside the container, already pointed at the emulator
ddev floci-aws url Prints both endpoints (--internal / --host for just one)
ddev floci-aws health Hits /_floci/health
ddev floci-aws services Per-service status from /_localstack/health
ddev floci-aws logs [-f] Container logs
ddev floci-aws shell A shell in the container
ddev floci-aws reset Deletes all emulated AWS state, and any containers Floci started for this project

ddev floci-aws aws needs the AWS CLI, which only the -compat image ships. If you switch FLOCI_AWS_IMAGE to floci/floci:latest for the smaller image, use your own AWS CLI against ddev floci-aws url --host instead.

Removing the add-on

Clean up first, remove second. ddev add-on remove deletes the ddev floci-aws command along with everything else, so reset is no longer available once the add-on is gone:

ddev floci-aws reset          # containers Floci started, plus the state volume
ddev add-on remove floci-aws

ddev add-on remove deliberately leaves three things behind, because none of them are the add-on’s to throw away:

If you skipped the reset and the add-on is already gone, the leftover containers Floci started can be cleared by hand. Keep the network filter — the label alone matches every project’s Floci containers, not just this one’s:

docker rm -f $(docker ps -aq \
  --filter label=floci_emulator=floci-aws \
  --filter network=ddev-<project>_default)

Licensing

Two different licences are in play here, and it is worth being explicit about which covers what:

No Floci source is vendored in this repository. The add-on only references the published floci/floci image, which Docker pulls at runtime; nothing here redistributes Floci code or binaries. If you redistribute this add-on you are redistributing Apache-2.0 material only.

Credits

Floci is by the floci-io project, MIT licensed. This add-on packages it under Apache 2.0; see Licensing above.

Behavioural notes in this README were verified against floci/floci:1.7.0 on 2026-08-26. Upstream ships frequently; the nightly workflow in .github/workflows/tests.yml re-runs the suite against latest every day, which is what catches drift.

Contributed and maintained by @codementality

If you find this add-on useful, please star it on GitHub — stars show appreciation and help maintainers know their work matters.