Published · Updated · By Elliot Jackson
Run a local S3 endpoint on macOS without Docker
Set up a native local S3-compatible endpoint on macOS, connect the AWS CLI, and verify a bucket and object round trip without a container runtime.
Use a local endpoint for the first development loop
If your application stores files in Amazon S3, the first development loop can become surprisingly elaborate. You need a bucket, credentials, a safe place for test objects, and enough confidence that a request is going where you think it is. For a small change, creating cloud infrastructure can be more ceremony than the change itself.
This guide covers the narrower case: you want an S3-shaped endpoint on a Mac, without Docker, so you can exercise one local client. The goal is a deliberately small proof: create a bucket, upload an object, download it again, and remove the fixture.
Objectively is the native macOS app I’m building to provide a local S3-compatible server for development. The public product page lists macOS 14+, a Homebrew install path, copy-ready connection details, a request feed, and an object browser. That ownership matters when you evaluate the recommendation: this is product-led guidance about my own app, not an independent review. It is not a claim that a local round trip proves every S3 behaviour your production application might use.
Before you start
You’ll need:
- a Mac running macOS 14 (Sonoma) or later;
- Homebrew installed;
- the AWS CLI, if you want to use the command-line walkthrough; and
- a small, non-sensitive fixture file.
At the time of writing, the published Homebrew cask definition is Objectively 0.2.0 and declares a Sonoma dependency. Check the current cask before following this guide, because both the version and macOS requirement can change.
The examples below use localhost:9000, which is the endpoint shown in Objectively’s public configuration examples. Use the endpoint, region, and credentials shown by your running environment if they differ.
Install the app
Install the cask from the published tap:
brew install elliotekj/dist/objectively
Then launch Objectively from Applications or Spotlight. The app is intended to keep the local service available from the macOS menu bar as well as its main window.
Check the first-launch details in the app instead of relying on a marketing page. Find the environment’s connection information and copy the values for:
- endpoint;
- access key ID;
- secret access key; and
- region.
Do not copy credentials from an article example into a real AWS profile. The values in this guide are placeholders for local development only.
Point the AWS CLI at the local server
The AWS CLI supports a service endpoint override. You can keep the local configuration in the current shell rather than changing your normal AWS profile:
export AWS_ACCESS_KEY_ID='objectively_<your-access-key>'
export AWS_SECRET_ACCESS_KEY='objectively_<your-secret-key>'
export AWS_ENDPOINT_URL='http://localhost:9000'
export AWS_REGION='us-east-1'
The endpoint is the important line. Without it, a command such as aws s3 ls may use your normal AWS configuration and talk to Amazon S3 instead of the local service. That is an unpleasant way to discover that a command was pointed at the wrong place.
For a one-off command, you can make the destination explicit instead:
aws --endpoint-url "$AWS_ENDPOINT_URL" s3 ls
AWS documents endpoint configuration through environment variables, shared configuration, CLI settings, and explicit client configuration. The exact precedence and supported options are described in the AWS endpoint configuration reference.
Verify a bucket and object round trip
The following commands check a small bucket-and-object workflow with the AWS CLI. Check the current installation instructions if the app’s setup changes between releases.
Use a unique, simple bucket name for the check. AWS's general-purpose bucket naming rules allow lower-case letters, numbers, hyphens, and dots; this local check does not establish complete AWS bucket-naming parity. The example also creates its bucket and temporary files for this invocation, then removes only those resources:
: "${AWS_ENDPOINT_URL:?Set AWS_ENDPOINT_URL from the running local environment}"
: "${AWS_ACCESS_KEY_ID:?Set AWS_ACCESS_KEY_ID from the running local environment}"
: "${AWS_SECRET_ACCESS_KEY:?Set AWS_SECRET_ACCESS_KEY from the running local environment}"
case "$AWS_ENDPOINT_URL" in
http://localhost:*) endpoint_port="${AWS_ENDPOINT_URL#http://localhost:}" ;;
http://127.0.0.1:*) endpoint_port="${AWS_ENDPOINT_URL#http://127.0.0.1:}" ;;
http://\[::1\]:*) endpoint_port="${AWS_ENDPOINT_URL#http://\[::1\]:}" ;;
*)
printf 'Refusing to run: AWS_ENDPOINT_URL must point to a loopback address.\n' >&2
exit 1
;;
esac
case "$endpoint_port" in
''|*[!0-9]*)
printf 'Refusing to run: AWS_ENDPOINT_URL must contain a numeric port.\n' >&2
exit 1
;;
esac
set -Eeuo pipefail
readonly endpoint="$AWS_ENDPOINT_URL"
run_id="$(uuidgen | tr '[:upper:]' '[:lower:]' | tr -d '-')"
bucket="objectively-check-$run_id"
readonly bucket
fixture=''
download=''
bucket_created=0
cleanup() {
exit_code=$?
trap - EXIT
if [ "$bucket_created" -eq 1 ]; then
aws --endpoint-url "$endpoint" s3 rm "s3://$bucket/fixture.txt" >/dev/null || true
aws --endpoint-url "$endpoint" s3 rb "s3://$bucket" >/dev/null || true
fi
if [ -n "$fixture" ]; then rm -f -- "$fixture"; fi
if [ -n "$download" ]; then rm -f -- "$download"; fi
exit "$exit_code"
}
trap cleanup EXIT
fixture="$(mktemp -t objectively-check)"
download="$(mktemp -t objectively-check-download)"
readonly fixture download
printf 'local S3 check\n' > "$fixture"
aws --endpoint-url "$endpoint" s3 mb "s3://$bucket"
readonly bucket_created=1
aws --endpoint-url "$endpoint" s3 cp "$fixture" "s3://$bucket/fixture.txt"
aws --endpoint-url "$endpoint" s3 ls "s3://$bucket/"
aws --endpoint-url "$endpoint" s3 cp "s3://$bucket/fixture.txt" "$download"
cmp "$fixture" "$download"
The cmp command compares the downloaded bytes with the file you uploaded. This confirms the bucket-and-object path shown here, not every S3 operation.
The EXIT trap removes the fixture files and, after a successful bucket creation, removes the known object and bucket. Keep the block scoped to this disposable check; if a command fails, the non-zero status is preserved after cleanup.
Use the app to confirm what the client did
The command line tells you whether the client received a response. The Objectively product page describes an object browser and a request feed for watching API calls live, which gives you another view of the local workflow.
After the commands finish, look for the matching bucket and object in the browser and compare what the interface shows with the client output. Treat the interface as supplementary evidence rather than assuming that a visible request establishes metadata, addressing style, or semantics your application expects. When an SDK reports a generic error, first establish whether a request reached the local endpoint, then inspect the client configuration.
A local success has a boundary
This setup is intended to answer one practical question: can this client perform the object flow described here against a local endpoint on this Mac?
It does not answer all of these:
- whether every S3 API operation your application uses is implemented;
- whether virtual-hosted-style addressing, TLS, versioning, policies, ACLs, CORS, tagging, lifecycle, encryption, or website operations behave as they do in AWS;
- whether the app is suitable for CI or headless environments; or
- whether a workflow tested locally will behave the same way against production S3.
Use the local endpoint as a fast, repeatable inner loop for the subset you actually exercise. Keep cloud-backed integration tests for the behaviours whose fidelity matters to your application.
If that is the workflow you need, see Objectively’s current Mac setup and install page. If you need a broader emulator, multiple AWS services, CI support, or a precise compatibility audit, this is the point to evaluate a different tool or keep the relevant tests in AWS.