> ## Documentation Index > Fetch the complete documentation index at: https://bunny.net/docs/llms.txt > Use this file to discover all available pages before exploring further. # Migrate from AWS S3 to Bunny Stream > A step-by-step walkthrough for migrating videos stored in an AWS S3 bucket into Bunny Stream using the Bunny CLI. > [!TIP] > > Run `bunny skills install --global` so AI coding agents, such as Claude Code, Cursor, and Codex, can run this migration for you. Then ask your agent: "Migrate my AWS S3 videos to Bunny Stream." > [!WARNING] > > `bunny stream import` is in [Public Preview](https://bunny.net/docs/product-release-stages.md#stage-2-public-preview) and still in development. Commands, flags, and output may change, so what you see can differ from the examples in this guide. To report a bug, [open an issue on the Bunny CLI repository](https://github.com/BunnyWay/cli/issues/new). [Bunny Stream](https://bunny.net/stream/) bills only for the storage and bandwidth you use. See the [Bunny Stream pricing page](https://bunny.net/pricing/stream/) for current rates. ## Overview ![Diagram of the Bunny CLI listing S3 objects and signing their URLs, and Bunny Stream fetching each file directly from the S3 bucket](https://mintcdn.com/bunnynet-cb9733c2/ZkttdKBCsq-yq0qO/images/stream/migrate-from-s3/how-bunny-stream-import-works.png?fit=max\&auto=format\&n=ZkttdKBCsq-yq0qO\&q=85\&s=13d8871dd5bd7b97e5d88997ebac3662) The Bunny CLI lists every video file in your bucket, creates a pre-signed download URL for each one, and hands it to Bunny Stream, which fetches the file directly from S3. Nothing is downloaded to your machine, and your bucket can stay private. Each top-level folder becomes a Bunny Stream collection with the same name. Each imported video is tagged with a hash of its bucket and key under the `s3Source` metatag, so running the import again skips videos that are already imported. S3 has no video catalog, so the CLI works out titles and collections from the object keys. For example, a bucket with one video at the top level and two folders maps like this: ``` migrate-to-bunny-demo/ Company intro.mp4 → "Company intro" in the library root Product demos/Dashboard tour.mp4 → "Dashboard tour" in the Product demos collection Product demos/Inviting your team.mp4 → "Inviting your team" in the Product demos collection Product demos/Setting up your first project.mp4 Webinars/Customer panel: scaling video.mp4 → "Customer panel: scaling video" in the Webinars collection Webinars/Q3 product update.mp4 ``` Folders nested deeper than the top level are not separate collections. A file at `Webinars/2024/Q3 product update.mp4` also lands in the Webinars collection. ## Scope The import copies the video files, their titles, any description and tags in the object metadata, and your top-level folder structure. Access controls stay on AWS, so plan to set up their Bunny equivalents. | S3 item | Migrated | Notes | | - | - | - | | Video file | Yes | The object as stored in S3. Files with these extensions are imported: `.mp4`, `.mov`, `.avi`, `.mkv`, `.webm`, `.flv`, `.wmv`, `.m4v`, `.mpg`, `.mpeg`, `.3gp`, `.ogv`, `.m2ts`, `.vod`, and `.amv`. | | Title | Yes | The file name without its extension, so `Product demos/Dashboard tour.mp4` becomes "Dashboard tour". | | Description | Yes | From the object's `x-amz-meta-description` metadata, stored in the `description` metatag on the Bunny video. | | Tags | Yes | From the object's `x-amz-meta-tags` metadata, a comma-separated list, stored in the `keywords` metatag on the Bunny video. | | Folders | Yes | Each top-level folder becomes a collection with the same name. Deeper folders roll up into their top-level collection. An existing collection whose name differs only in case is reused. | | Object identity | Yes | A SHA-256 hash of `bucket/key`, stored in the `s3Source` metatag on the Bunny video. Renaming or moving an object makes it a new video, which the next run imports again. | | `.ts` files | Not by default | They are skipped, because HLS output holds thousands of `.ts` segments. Set `S3_INCLUDE_TS=true` to import them as videos. | | Objects in Glacier, Deep Archive, or an Intelligent-Tiering archive tier | Not until restored | Restore them in S3 first. Objects in Glacier Instant Retrieval import normally. | | Videos above 2160p or longer than 72 hours | No | Bunny Stream rejects them. S3 does not report resolution or duration, so the dry run cannot warn about them. | | Bucket policies, presigned links, and CloudFront signed URLs | No | See the privacy note below. | | Captions and subtitle files | No | Upload them with `bunny stream videos captions add`, or generate new ones with [transcription](https://bunny.net/docs/stream/transcribing.md). | | Thumbnails | No | Bunny generates new thumbnails when it encodes each video. | > [!WARNING] > > **Private S3 videos are not private on Bunny.** Your bucket can stay private during the import, because Bunny fetches each file through a pre-signed URL. On Bunny, though, every imported video follows the security settings of its library, and a new library allows playback from any site. A video that only signed-in users could reach on S3 or CloudFront plays for anyone with its URL until you protect the library. Before you publish the new URLs, set up [Allowed domains](https://bunny.net/docs/stream/security-options.md#domains), and turn on [embed view token authentication](https://bunny.net/docs/stream/token-authentication.md) or [CDN token authentication](https://bunny.net/docs/stream/security.md#cdn-token-authentication) for videos that only signed-in viewers should see. If those videos are in their own top-level folders, you can import those folders into a separate library with `--folder` and give that library stricter settings. ## Prerequisites * A [bunny.net account](https://dash.bunny.net/auth/register) * An [AWS](https://console.aws.amazon.com) account with an S3 bucket that holds your videos, and permission to create an IAM user ## Migrate your videos **1. Install and authenticate the Bunny CLI** Install the Bunny CLI with the shell installer or npm: > [tab] curl > > ```sh theme={null} > curl -fsSL https://cli.bunny.net/install.sh | sh > ``` > > The installer downloads a prebuilt binary for your platform and places it on your `PATH`. > [tab] npm > > ```sh theme={null} > npm install -g @bunny.net/cli > ``` See [Installation](https://bunny.net/docs/cli/installation.md) for more options. Then authenticate the CLI with your bunny.net account: ```sh theme={null} bunny login ``` `bunny login` opens your browser to complete authentication and stores the credentials locally. The first time you log in from an interactive terminal, it also offers to install the agent skill for AI coding tools. On a remote machine or in CI without a browser, run `bunny login --api-key "$BUNNYNET_API_KEY"` instead, with a key from [Account Settings > API key](https://dash.bunny.net/account/api-key). **2. Create a Bunny Stream library** Create a library for the imported videos, or skip this step to use an existing one: ```sh theme={null} bunny stream libraries create migrate-from-s3 --replication-regions NY,SG ``` ```sh theme={null} ✓ Created video library migrate-from-s3 (ID: 12345), replicated to DE, NY, SG. ``` DE is always the main storage region. Replicate to the regions closest to your audience, from `UK`, `SE`, `NY`, `LA`, `SG`, `SYD`, `BR`, and `JH`. You can't remove a [replication region](https://bunny.net/docs/stream/replication.md) later. > [!NOTE] > > Set encoding options, such as `--resolutions` or `--encoding-tier`, when you create the library. Changes you make later apply only to new uploads, not to videos that are already imported. Run `bunny stream libraries create --help` to see every option, or read [Encoding](https://bunny.net/docs/stream/encoding.md). When the CLI asks how to add videos, choose **Later**. You start the import in the next steps. Copy the library ID from the output. You pass it to `--library` in the import commands. The flag also accepts the library's name, and `--lib` works as a short alias. **3. Create an IAM user with read access to your bucket** The Bunny CLI needs an AWS access key that can list your bucket and read its objects. It never changes anything in S3. In the [IAM console](https://console.aws.amazon.com/iam/), go to **Users** and click **Create user**. Enter a user name, such as migrate-to-bunny, leave console access off, and click **Next**. ![The Specify user details step of Create user in the AWS IAM console with the user name migrate-to-bunny](https://mintcdn.com/bunnynet-cb9733c2/ZkttdKBCsq-yq0qO/images/stream/migrate-from-s3/iam-create-user.png?fit=max\&auto=format\&n=ZkttdKBCsq-yq0qO\&q=85\&s=93e783516250c09d87ca8bb6b437ec93) On **Set permissions**, choose **Attach policies directly**, then click **Create policy**. In the policy editor, switch to **JSON** and paste the following policy, replacing `migrate-to-bunny-demo` with your bucket name in both places: ```json theme={null} { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": "s3:ListBucket", "Resource": "arn:aws:s3:::migrate-to-bunny-demo" }, { "Effect": "Allow", "Action": "s3:GetObject", "Resource": "arn:aws:s3:::migrate-to-bunny-demo/*" } ] } ``` Name the policy, such as migrate-to-bunny-demo, and create it. Back on **Set permissions**, refresh the policy list, select the new policy, then click **Next** and **Create user**. > [!NOTE] > > If your bucket encrypts objects with SSE-KMS, also allow `kms:Decrypt` on the bucket's KMS key. Buckets with the default SSE-S3 encryption need nothing more. Open the new user, go to **Security credentials**, and click **Create access key**. Choose **Command Line Interface (CLI)** as the use case, confirm the recommendation, and create the key. Copy the **Access key ID** and the **Secret access key**. AWS shows the secret only once. Set the access key in the shell where you run the import: ```sh theme={null} export AWS_ACCESS_KEY_ID=your-access-key-id export AWS_SECRET_ACCESS_KEY=your-secret-access-key ``` The CLI does not prompt for or save access keys. If you already use the AWS CLI, it also picks up keys from `~/.aws/credentials` and the profile `AWS_PROFILE` names. Pre-signed URLs made from temporary credentials, such as an AWS SSO session, stop working when the session ends, so use an access key for a large import. **4. Preview the migration with a dry run** Run the import with `--dry-run`, using the library ID from step 2: ```sh theme={null} bunny stream import --library 12345 --source s3 --dry-run ``` `--dry-run` prints the import plan without making any changes to either platform. The CLI then prompts for your bucket's region and name. The region prompt is filled in with `us-east-1`, so replace it with your bucket's region, which the S3 console shows in the bucket list: ```sh theme={null} AWS S3 credentials ✔ AWS Region: … your-bucket-region ✔ S3 Bucket: … migrate-to-bunny-demo ``` Once S3 accepts them, the CLI offers to save them so later runs skip these prompts. Press Enter to accept: ```sh theme={null} ✔ Save AWS S3 credentials for next time? … yes ✓ Saved AWS S3 credentials to the "default" profile. ``` > [accordion] Where the CLI stores credentials > > The CLI saves the region and bucket for the active Bunny CLI profile in `~/.config/bunnynet/stream-import-credentials.json` (or under `$XDG_CONFIG_HOME/bunnynet/` when it is set). Your access keys are not saved. The `AWS_REGION` and `S3_BUCKET` environment variables, and the `--bucket` flag, always take priority over saved values. The dry run then prints a summary and the import plan: ```sh theme={null} AWS S3 to Bunny Stream Key Value Library migrate-from-s3 (12345) Source AWS S3: 6 videos in 2 folders Already imported 0 To import 6 Size 17 MB Import plan Product demos (3 videos) - Dashboard tour - Inviting your team - Setting up your first project Webinars (2 videos) - Customer panel: scaling video - Q3 product update (no collection) (1 videos) - Company intro ℹ Dry run. Remove --dry-run to import for real. ``` **Already imported** counts videos that are already in the library, and **To import** counts the ones a real run would import. S3 does not know how long a video is, so the summary has no Duration row. It does not report resolution either, so unlike other sources, the dry run cannot warn about videos above Bunny's 2160p or 72-hour limits. Those videos fail when Bunny encodes them. The plan groups videos by the collection they will land in, and lists them in alphabetical order of their keys, the order S3 returns them in. Videos at the top level of the bucket are listed under (no collection). To import only part of a bucket, add `--prefix` with the path to start from, such as `--prefix videos/2024`, or set `S3_PREFIX`. The top-level folders below that prefix then become the collections. **5. Run the import** Run the same command without `--dry-run`: ```sh theme={null} bunny stream import --library 12345 --source s3 ``` The CLI prints the same summary, then asks for confirmation. Type `y` and press Enter to start: ```sh theme={null} ? Import 6 videos into migrate-from-s3? › (y/N) ``` The CLI creates a collection for each top-level folder, skips videos that are already imported, hands the rest to Bunny three at a time, and tags each video with `s3Source`, along with any description and tags from its metadata. It returns once every video is queued. Bunny then fetches each file directly from S3 and encodes it. ```sh theme={null} ✔ Import 6 videos into migrate-from-s3? … yes ✓ Queued 6 videos into migrate-from-s3. Bunny is fetching and encoding them now. ℹ Check progress with bunny stream import status --library 12345 --source s3 ``` Collection names match without regard to case, so the S3 folders `Webinars/` and `webinars/` land in one collection, even though S3 treats them as separate folders. Move the objects in one of them to a differently named folder before the import to keep them separate. Each pre-signed URL stays valid for six hours by default, which is long enough for Bunny to fetch the file. Set `S3_PRESIGNED_URL_TTL` to a number of seconds between 3600 and 604800 to change it. The import command also accepts these flags: * `--wait` keeps the command running, with a live encoding progress indicator, until Bunny has encoded every video. * `--force` skips the confirmation prompt and the offer to retry failed videos, for unattended or CI runs. * `--concurrency N` sets how many videos the CLI passes to Bunny in parallel, from 1 to 20. * `--clean-failed` deletes the failed Bunny copies of videos before importing them again, including videos that Bunny rejected. * `--retry-stalled` deletes and imports again any video stuck at zero bytes on Bunny. * `--request-timeout` sets how many seconds one request to S3 or Bunny can take before it gives up (default 30). * `--video-timeout` sets how many seconds the hand-off of one video to Bunny, or with `--wait` its encoding, can take before it gives up (default 5400). Run `bunny stream import --help` for the full list. To migrate a large bucket one folder at a time, list the folders: ```sh theme={null} bunny stream import list --source s3 ``` ```sh theme={null} ID Name Videos Product demos Product demos 3 Webinars Webinars 2 1 videos are not in a folder. Import one folder with bunny stream import --source s3 --folder ``` The ID of a folder is its path in the bucket. Preview a folder's videos with `--folder`, then pass the same ID to the import. Quote the ID when it contains spaces: ```sh theme={null} bunny stream import list --source s3 --folder "Product demos" bunny stream import --library 12345 --source s3 --folder "Product demos" ``` The import works the same way, limited to that folder's videos and its collection. `--folder` also accepts a deeper path, such as `"Webinars/2024"`. Its videos still land in the top-level collection, Webinars. **6. Watch encoding progress** The import returns before Bunny finishes encoding. Check progress with the status command: ```sh theme={null} bunny stream import status --library 12345 --source s3 ``` It prints one row per queued video, with its status on Bunny, encode percentage, size, and when it was queued: ```sh theme={null} AWS S3 to migrate-from-s3 Video Bunny status Encoded Size Queued Dashboard tour Processing 5% just now Inviting your team Processing 5% just now Setting up your first project Processing 5% just now Customer panel: scaling video Processing 5% just now Q3 product update Processing 5% just now Company intro Processing 5% just now ℹ 0 finished, 6 processing, 0 failed. ``` Run it again until the last line reports 0 processing. The command exits with a non-zero code when any video has failed, so you can also use it as a check in scripts. **7. Verify in the bunny.net dashboard** Open the [bunny.net dashboard](https://dash.bunny.net) and go to **Stream > Your library > Manage library**. Each top-level S3 folder appears as a collection, and every migrated video is listed under **Latest videos**. ![The Manage library page in the bunny.net dashboard showing the imported collections and videos](https://mintcdn.com/bunnynet-cb9733c2/ZkttdKBCsq-yq0qO/images/stream/migrate-from-s3/manage-library-videos.png?fit=max\&auto=format\&n=ZkttdKBCsq-yq0qO\&q=85\&s=c0528b569150df2a53ec8cd327e6091d) Set up the library's [security settings](https://bunny.net/docs/stream/security-options.md) if you have not already, then replace the S3 or CloudFront video URLs in your site with their Bunny Stream equivalents. See [Embedding videos](https://bunny.net/docs/stream/embedding.md) for the embed and player options. Once the migration is complete, you can delete the IAM user's access key, or the user itself. ## Troubleshooting > [accordion] Some videos failed during the import > > When videos fail during an interactive import, the CLI lists each failure and offers to retry them: > > ```sh theme={null} > ✖ Training archive: S3 object is in GLACIER; restore it in S3 before importing > ℹ Resume with bunny stream import --library 12345 --source s3 --resume > ? 1 videos failed. Retry them now? › (y/N) > ``` > > Answer `y` to retry them in the same run. If the import was interrupted, or you declined the retry, run it again with `--resume`: > > ```sh theme={null} > bunny stream import --library 12345 --source s3 --resume > ``` > > `--resume` retries failed videos, processes any that were still outstanding, and re-applies the `s3Source` tag to videos Bunny fetched before the run stopped. When the interrupted run was limited to one folder, the resume continues that folder. It stops retrying a video after three failed attempts and skips videos that Bunny rejected. To retry those too, add `--clean-failed`, which deletes their Bunny copies first. > > Running the import again is always safe. Videos that already carry the `s3Source` tag are skipped, and the CLI reports **Everything is already imported** once nothing is left. > [accordion] Bucket is in a different region > > ```sh theme={null} > ✖ Bucket "migrate-to-bunny-demo" is in a different region. > Set AWS_REGION to the bucket's region. > ``` > > The region you entered, or the default `us-east-1`, is not the bucket's region. Find the region in the S3 console's bucket list, then run the command again with it set: > > ```sh theme={null} > AWS_REGION=your-bucket-region bunny stream import --library 12345 --source s3 --dry-run > ``` > > `AWS_REGION` takes priority over a saved region. To replace the saved one, answer yes when the CLI offers to save again. > [accordion] Access denied to bucket > > ```sh theme={null} > ✖ Access denied to bucket "migrate-to-bunny-demo". > The credentials need s3:ListBucket and s3:GetObject. > ``` > > The access key works, but its policy does not cover this bucket. Check that the bucket name in the IAM policy from step 3 matches your bucket exactly, in both `Resource` lines. > [accordion] Invalid AWS credentials > > ```sh theme={null} > ✖ Invalid AWS credentials. > ``` > > AWS rejected the access key, or the CLI found no credentials at all. Check that `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` are set in the shell where you run the import, and that the key is still active in the IAM console. > [accordion] S3 object is in GLACIER > > The object is in an archive storage class, which S3 cannot serve until it is restored. Restore it from the S3 console, wait for the restore to finish, then run the import again. Until then, the error says the restore is still in progress. > [accordion] Videos are stuck with no data > > If `bunny stream import status` reports videos stuck at zero bytes, run the import again with `--retry-stalled` to delete and fetch them again. ## Migrate from an S3-compatible provider The `s3` source also works with S3-compatible storage, such as Cloudflare R2, Wasabi, Backblaze B2, or MinIO. Set `S3_ENDPOINT` to the provider's `https://` endpoint, and use the provider's region name, such as `auto` for R2: ```sh theme={null} export S3_ENDPOINT=https://your-account-id.r2.cloudflarestorage.com export AWS_REGION=auto bunny stream import --library 12345 --source s3 --dry-run ``` Use an access key from the provider with read access to the bucket. Everything else in this guide works the same way. This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.