Deploy Build Artifacts to Cloudflare R2 with GitHub Actions

Deploy to Cloudflare R2 with GitHub Actions

Cloudflare R2 is one of the best places to store build artifacts, static site deployments, and media assets. There are no egress fees — meaning you can download your artifacts as often as you like without paying a cent in bandwidth costs. For teams that push builds frequently, that adds up fast compared to S3 or GCS.

With the R2Drop CLI, you can add Cloudflare R2 uploads to any GitHub Actions workflow in a single step. This guide walks through the complete setup: creating a scoped API token, configuring secrets, writing the workflow YAML, and capturing the upload URL for downstream steps.

Prerequisites

Before you start, make sure you have:

  • A Cloudflare account with R2 enabled (you can activate it from the R2 section of the Cloudflare dashboard)
  • An R2 bucket already created — for example, my-build-artifacts
  • A Cloudflare API token with R2:Edit permission (we'll create this below)
  • A GitHub repository with Actions enabled

The R2Drop CLI runs on macOS. For CI environments, use the install script which supports macOS runners. GitHub's macos-latest runners work perfectly out of the box.

Creating a Cloudflare API Token

You should never use your global Cloudflare API key for CI/CD. Instead, create a minimal-permission token scoped only to R2. Here's how.

Step 1: Open API Tokens

Go to dash.cloudflare.com/profile/api-tokens (My Profile → API Tokens). Click Create Token.

Step 2: Start from a custom template

Scroll past the pre-built templates and click Create Custom Token at the bottom. This gives you full control over the scope.

Step 3: Set the permissions

Under Permissions, add one entry:

  • Resource type: Account
  • Service: Workers R2 Storage
  • Permission level: Edit

Under Account Resources, select the specific Cloudflare account that owns your R2 bucket. Avoid selecting “All accounts” — keep the scope as tight as possible.

Step 4: Save and copy the token

Click Continue to Summary, then Create Token. Cloudflare shows the token once — copy it immediately and store it somewhere safe. You'll paste it into GitHub Secrets in the next section.

Cloudflare R2 uses an S3-compatible API. The access key ID and secret access key you need for R2Drop are not the same as the API token itself. To get them, go to your R2 bucket's Settings tab in the Cloudflare dashboard and click Manage R2 API Tokens. Create a token there with Object Read & Write scope. That gives you the Access Key ID and Secret Access Key pair that R2Drop and the S3 SDK use.

Setting Up GitHub Actions Secrets

Navigate to your GitHub repository, then go to Settings → Secrets and variables → Actions → New repository secret. Add the following four secrets:

  • R2_ACCESS_KEY_ID — Access Key ID from R2 API token page
  • R2_SECRET_ACCESS_KEY — Secret Access Key from R2 API token page
  • R2_BUCKET — Your bucket name, e.g. my-build-artifacts
  • R2_ENDPOINT — Your R2 S3 endpoint, e.g. https://<account-id>.r2.cloudflarestorage.com

These secrets are injected as environment variables at runtime. They are never exposed in logs, and they are never visible to pull requests from forks — so they're safe to use in public repositories as long as you keep your workflow scoped to trusted branches.

Full GitHub Actions Workflow

Here's a complete, working workflow that builds a project and uploads the ./dist folder to Cloudflare R2 on every push to main.

name: Build and Deploy to R2

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: macos-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Build project
        run: npm ci && npm run build

      - name: Install R2Drop CLI
        run: curl -sf https://r2drop.com/install.sh | sh

      - name: Write R2Drop config
        run: |
          mkdir -p ~/.r2drop
          cat > ~/.r2drop/config.toml <<EOF
          [accounts.ci]
          bucket = "${{ secrets.R2_BUCKET }}"
          endpoint = "${{ secrets.R2_ENDPOINT }}"
          access_key_id = "${{ secrets.R2_ACCESS_KEY_ID }}"
          secret_access_key = "${{ secrets.R2_SECRET_ACCESS_KEY }}"

          [defaults]
          account = "ci"
          EOF

      - name: Upload dist to R2
        id: upload
        run: |
          RESULT=$(r2drop upload ./dist --recursive --account ci --json)
          echo "result=$RESULT" >> "$GITHUB_OUTPUT"
          echo "$RESULT" | jq -r '"Uploaded to: " + .url'

      - name: Print upload URL
        run: echo "Build artifacts live at ${{ fromJSON(steps.upload.outputs.result).url }}"

Using the --json Flag to Capture Upload URLs

The --json flag is the key to making uploads scriptable. When you pass it, r2drop outputs a JSON object instead of human-readable text:

{
  "status": "success",
  "key": "dist/",
  "url": "https://cdn.example.com/dist/",
  "bucket": "my-build-artifacts",
  "files_uploaded": 42,
  "size": 8421376,
  "duration_ms": 3102
}

Alternative: Wrangler for R2 Uploads in CI

Cloudflare's own wrangler CLI also supports R2 uploads via wrangler r2 object put. The main trade-offs compared to R2Drop CLI are:

  • Wrangler uploads one object at a time — there's no native recursive folder upload or multipart parallelism built in
  • Wrangler is a Node.js package, adding install time and a larger dependency footprint on your runner
  • R2Drop CLI is a single static binary — faster to install, no Node.js required

Advanced: Upload Only on Git Tags

For release workflows, you often want to upload artifacts only when you push a version tag — not on every commit. Change the on trigger in your workflow like this:

on:
  push:
    tags:
      - 'v*'

Then you can include the tag name in the upload path to version your artifacts:

- name: Upload release artifact
  run: |
    r2drop upload ./dist \
      --recursive \
      --account ci \
      --key "releases/${{ github.ref_name }}/" \
      --json

Advanced: Parallel Uploads with a Matrix Strategy

If you're building for multiple platforms, you can compile and upload each platform artifact in parallel using a matrix strategy:

jobs:
  build-and-upload:
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        include:
          - os: macos-latest
            platform: macos-arm64

    steps:
      - uses: actions/checkout@v4
      - name: Build
        run: cargo build --release
      - name: Install R2Drop CLI
        run: curl -sf https://r2drop.com/install.sh | sh
      - name: Upload binary to R2
        shell: bash
        run: |
          r2drop upload ./target/release/mybinary \
            --account ci \
            --key "releases/${{ github.ref_name }}/mybinary-${{ matrix.platform }}" \
            --json

Each matrix job uploads its platform-specific binary to a distinct key under the same release path. Use the --json flag to capture structured output for downstream steps.

Local Development: Use the R2Drop macOS App

The CLI is designed for headless, automated use. But when you're actively developing — iterating on a build, testing assets, or manually pushing files — the R2Drop macOS app is significantly faster.

Right-click any file or folder in Finder, choose Send to R2, and the upload starts immediately. The public URL is copied to your clipboard automatically when it finishes. It uses the same ~/.r2drop/config.toml as the CLI, so any account you configure in the app is instantly available from the command line — and vice versa.

Think of the macOS app and CLI as complementary tools: the app for fast, interactive uploads during development, and the CLI for automated deploys in CI. Read the full CLI guide for a complete reference of all flags and configuration options.

R2Drop desktop app showing upload queue for local development uploads
R2Drop desktop app for local development uploads
R2Drop CLI Guide
CLI
R2Drop CLI: Terminal Uploads to Cloudflare R2

Upload files to Cloudflare R2 from the terminal, shell scripts, and CI/CD pipelines with the R2Drop CLI.

February 25, 2026
Upload files to Cloudflare R2 from macOS
Workflow
How to Upload Files to Cloudflare R2 from macOS

Skip the Cloudflare dashboard. Upload to R2 directly from Finder, the menu bar, or the terminal with R2Drop.

February 26, 2026