Getting startedQuickstart
Quickstart
Install s3mirror, describe both endpoints in a config file, check it, run a copy-only mirror, then turn deletion on.
Install
Section titled “Install”brew tap netspeedy/s3mirrorbrew install s3mirrormacOS and Linux. If Homebrew refuses the new third-party tap, runbrew trust netspeedy/s3mirror once, then install again.
docker pull ghcr.io/netspeedy/s3mirror:v1.1.0docker run --rm ghcr.io/netspeedy/s3mirror:v1.1.0 --versionlinux/amd64 and linux/arm64 on GHCR. Mount your config into the container and pass the same flags; Container has the details.
git clone https://github.com/netspeedy/s3mirror.gitcd s3mirrorpython3 -m venv .venv. .venv/bin/activatepython -m pip install -r requirements.txtPython 3.11 or newer. The command is then python3 s3mirror.py rather thans3mirror.
Check it’s on your path:
s3mirror --versionDescribe both endpoints
Section titled “Describe both endpoints”s3mirror has no useful defaults for the endpoints, so it always needs a config file. Put the source
and destination in config.yaml. Anything you leave out takes its default, so this is enough to
start:
source: endpoint_url: "https://s3.source.example.com" aws_access_key_id: "SOURCE_ACCESS_KEY" aws_secret_access_key: "SOURCE_SECRET_KEY"
destination: endpoint_url: "https://s3.destination.example.com" aws_access_key_id: "DEST_ACCESS_KEY" aws_secret_access_key: "DEST_SECRET_KEY"TLS certificates are checked by default. For an endpoint with a self-signed certificate or one from
a private CA, set verify_ssl to the CA bundle’s path, as Safety
shows. The file holds both secret keys: keep it readable only by the account that runs s3mirror, for
example with chmod 600 config.yaml.
Print the configuration s3mirror will actually use, with your file merged over the defaults:
s3mirror --config config.yaml --show-config{ "source": { "endpoint_url": "https://s3.source.example.com", "aws_access_key_id": "SOURCE_ACCESS_KEY", "aws_secret_access_key": "***REDACTED***", "region_name": "us-east-1", "verify_ssl": true }, "destination": { "endpoint_url": "https://s3.destination.example.com", "aws_access_key_id": "DEST_ACCESS_KEY", "aws_secret_access_key": "***REDACTED***", "region_name": "us-east-1", "verify_ssl": true }, "performance": { "max_workers": 20, "multipart_threshold": 8388608, "multipart_chunksize": 8388608, "max_concurrency": 10, "max_pool_connections": 50 }, "sync": { "delete_extraneous": true, "exclude_buckets": [] }}Note "delete_extraneous": true at the bottom. That’s the default, and it means a normal run
deletes destination objects the source doesn’t have.
Run it copy-only
Section titled “Run it copy-only”Make the first run copy-only, so nothing at the destination is deleted:
s3mirror --config config.yaml --no-deleteIn this example the source has two buckets. The destination already has app-data, holding one
matching export, an older copy of config/settings.json and a stray exports/old-report.csv, and
doesn’t have media-assets at all:
======================================================================S3 BUCKET MIRRORSession started: 2026-11-05 08:10:53======================================================================✓ Source connected successfully (2 buckets)✓ Destination connected successfully (1 buckets)
Starting synchronization of 2 bucket(s)Performance: 20 workers, 8.0MB multipart threshold
======================================================================[1/2]: app-data====================================================================== Source: 4 objects Destination: 3 objects Actions: 3 to copy (100.8KB), 0 to delete ✓ Copied 3 objects in 0.0s (39.8MB/s) ✓ Bucket completed in 0.0s
======================================================================[2/2]: media-assets====================================================================== ✓ Created bucket: media-assets Source: 2 objects Destination: 0 objects Actions: 2 to copy (742.2KB), 0 to delete ✓ Copied 2 objects in 0.0s (40.3MB/s) ✓ Bucket completed in 0.0s
======================================================================FINAL SUMMARY======================================================================Started: 2026-11-05 08:10:53Finished: 2026-11-05 08:10:53Duration: 0.0s (0s)
Buckets: - Processed: 2 - Created: 1
Objects: - Copied: 5 - Deleted: 0
Data transferred: 843.0KBAverage throughput: 36.6MB/s
✓ Status: COMPLETED SUCCESSFULLY======================================================================The two new exports and the changed settings.json were copied, the matching export was skipped,
and media-assets was created and filled. The stray old-report.csv is still there, because
--no-delete was set. These runs were made against an in-process stand-in for the two endpoints,
which is why the transfers are so quick; the output is otherwise exactly what s3mirror prints.
Add --debug to see each decision, such as which objects counted as new and which as changed.
Logging shows what that looks like.
Let it delete
Section titled “Let it delete”When the copy-only run looks right, run it again without --no-delete:
s3mirror --config config.yamlThis time nothing needs copying, and the object that exists only at the destination goes:
======================================================================[1/2]: app-data====================================================================== Source: 4 objects Destination: 5 objects Actions: 0 to copy (0.0B), 1 to delete ✓ Deleted 1 objects in 0.0s ✓ Bucket completed in 0.0sAt the normal log level s3mirror only reports how many objects it deleted. Run with --debug or
--log-file to get each key.
Next steps
Section titled “Next steps”-
Give s3mirror credentials that can only do what the mirror needs. Safety lists the permissions each side uses.
-
Set the options you care about in the config, such as
exclude_bucketsor a lowermax_workers. Configuration walks through the file. -
Schedule it with
--log-file, and alert when it exits with anything other than 0. Scheduling has cron and systemd examples.