copy¶
Copy an image to S3 from any OCI registry or another S3 bucket — without going through the local Docker daemon.
Arguments¶
| Argument | Description |
|---|---|
<src> |
Source image. S3/local reference or OCI registry reference (see formats below). |
<s3-dest> |
Destination in s3://bucket/image:tag or local://path/image:tag format. Tag is required for both S3/local source and destination. |
Flags¶
| Flag | Description |
|---|---|
--platform <os/arch> |
Copy only the specified platform, e.g. linux/amd64. Default: copy all platforms. |
--force |
Overwrite an existing destination tag even if immutability is enabled |
Source formats¶
| Format | Example |
|---|---|
| Bare name (Docker Hub official) | alpine, nginx:1.25 |
| User image (Docker Hub) | user/myapp:v1.0 |
| Full registry | ghcr.io/owner/image:tag |
| ECR | 123456789.dkr.ecr.us-east-1.amazonaws.com/myapp:v1.0 |
| S3 | s3://source-bucket/myapp:v1.0 |
| Local | local://./local-s3/myapp:v1.0 |
Bare names resolve exactly like docker pull — alpine becomes docker.io/library/alpine:latest.
Examples¶
# Mirror from Docker Hub (bare name, like docker pull)
s3lo copy alpine:latest s3://my-bucket/alpine:latest
s3lo copy nginx:1.25 s3://my-bucket/nginx:1.25
# Mirror a specific platform only
s3lo copy alpine:latest s3://my-bucket/alpine:latest --platform linux/amd64
# Copy from ECR (auto-authenticates with your AWS credentials)
s3lo copy 123456789.dkr.ecr.us-east-1.amazonaws.com/myapp:v1.0 s3://my-bucket/myapp:v1.0
# Copy from GitHub Container Registry
s3lo copy ghcr.io/owner/myapp:v1.0 s3://my-bucket/myapp:v1.0
# Promote between S3 buckets (server-side copy within same bucket — free and instant)
s3lo copy s3://staging-bucket/myapp:v1.0 s3://prod-bucket/myapp:v1.0
# Copy from local storage to S3
s3lo copy local://./local-s3/myapp:v1.0 s3://my-bucket/myapp:v1.0
# Overwrite an immutable destination tag
s3lo copy s3://staging-bucket/myapp:v1.0 s3://prod-bucket/myapp:v1.0 --force
Multi-arch behavior¶
By default, copy copies all platforms from a multi-arch image and preserves the OCI Image Index at the destination. Users pulling from the destination will get the full multi-arch image.
Use --platform to copy only one platform (results in a single-arch image at the destination):
# Copy only linux/amd64 from a multi-arch alpine
s3lo copy alpine:latest s3://my-bucket/alpine:latest --platform linux/amd64
Signatures¶
A signature covers the digest of the manifest bytes stored at
manifests/<image>/<tag>/manifest.json. What happens to it on a copy follows
from that:
| Copy | Manifest at the destination | Signature |
|---|---|---|
| Bucket → bucket, all platforms | byte for byte identical | travels, still verifies |
Bucket → bucket, --platform |
rewritten to the filtered index | dropped, and copy says so |
| Registry → bucket | written from the registry's manifest | none exists to carry; sign the copy |
A stale signature is worse than no signature: the image would claim to be signed and then fail to verify. So when the manifest is rewritten the signature is left behind and the copy tells you to re-sign:
Warning: 1 signature(s) not copied — filtering platforms rewrites the manifest,
so the old signature cannot verify against it. Re-sign the copy:
s3lo sign s3://my-bucket/myapp:amd64 --key <key>
How it works¶
- Fetches all platform manifests from the registry in parallel (using the OCI Distribution API).
- Deduplicates blobs across platforms — shared layers are uploaded only once.
- Uploads all unique blobs to S3 in parallel (up to 10 concurrent workers).
- Writes the manifest to S3.
For ECR: calls ecr:GetAuthorizationToken using your AWS credentials.
For Docker Hub and others: uses the standard Bearer token challenge flow.
Uses S3 server-side CopyObject — no data transfer, no egress cost, near-instant.
Streams each blob from the source bucket to the destination. Blobs already present at the destination are skipped.