Status: Beta. This action is still maturing. The interface is stable for the v1 line, but you may encounter edge cases - please open an issue if you do. Use in production with caution and pin to an exact tag (
@v1.0.0) rather than the floating major if you need reproducibility.
Upload, verify, list, and prune backup objects in Cloudflare R2 from GitHub Actions. Composite shell action plus a reusable workflow that orchestrates server backups end to end - source prep, transport, R2 upload, retention, integrity verification.
Part of the NXTools Collection by NX1X.
- uses: NX1X/Cloudflare-R2-backup-action@v1
with:
operation: upload
account-id: ${{ secrets.CF_ACCOUNT_ID }}
access-key-id: ${{ secrets.R2_ACCESS_KEY_ID }}
secret-access-key: ${{ secrets.R2_SECRET_ACCESS_KEY }}
bucket: my-backups
prefix: daily-backups/
file: ./myapp_backup.tar.gz
retention-days: '90'This uploads the file to s3://my-backups/daily-backups/myapp_backup.tar.gz and deletes anything in daily-backups/ older than 90 days.
For a turn-key backup pipeline (archive -> upload -> optional verify), use the included reusable workflow with a built-in DB template:
jobs:
backup:
uses: NX1X/Cloudflare-R2-backup-action/.github/workflows/backup.yml@v1
with:
source-mode: tunnel-ssh
ssh-host: ${{ vars.TUNNEL_HOSTNAME }}
ssh-user: deploy
db-template: mysql # or postgres, mongodb, sqlite
db-dump-binary: mariadb-dump # MariaDB variant
db-user: root
db-docker-compose-service: db # dump runs inside `docker compose exec`
db-docker-compose-dir: /home/deploy/myapp
extra-paths: |
/home/deploy/myapp/data/uploads
bucket: my-backups
prefix: daily/
retention-days: '90'
verify-after-upload: true
secrets:
R2_ACCOUNT_ID: ${{ secrets.CF_ACCOUNT_ID }}
R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
CF_ACCESS_CLIENT_ID: ${{ secrets.CF_ACCESS_CLIENT_ID }}
CF_ACCESS_CLIENT_SECRET: ${{ secrets.CF_ACCESS_CLIENT_SECRET }}
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
DB_PASSWORD: ${{ secrets.DATABASE_ROOT_PASSWORD }}That single caller replaces ~190 lines of hand-rolled cloudflared + s3cmd + retention boilerplate.
| Operation | Purpose |
|---|---|
upload |
Upload a single file to a bucket/prefix. Optional inline retention prune. |
prune |
Delete objects under a prefix by age (retention-days) and/or count (retention-count). |
verify |
Download an object and check integrity: tar, zip, or sha256 against an expected digest. |
list |
List objects under a prefix as a JSON array, plus count and total-size. |
| Source mode (reusable workflow) | Where the backup script runs |
|---|---|
local |
The GitHub runner |
ssh |
A remote server reached over plain SSH |
tunnel-ssh |
A server behind a Cloudflare Tunnel (wraps cloudflare-tunnel-ssh-action) |
| DB template | Engine | Tool |
|---|---|---|
mysql |
MySQL & MariaDB | mysqldump (or mariadb-dump) |
postgres |
PostgreSQL | pg_dump (single DB) or pg_dumpall |
mongodb |
MongoDB | mongodump --archive --gzip |
sqlite |
SQLite | sqlite3 .backup (hot backup) |
| Input | Required | Default | Description |
|---|---|---|---|
operation |
yes | - | upload, prune, verify, or list |
account-id |
yes | - | Cloudflare account ID (used to derive the R2 endpoint) |
access-key-id |
yes | - | R2 API access key ID |
secret-access-key |
yes | - | R2 API secret access key |
bucket |
yes | - | R2 bucket name |
prefix |
no | '' |
Path prefix inside the bucket (e.g. daily-backups/). Trailing slash auto-added. |
file |
conditional | - | Local file path. Required for upload. |
object-key |
conditional | - | Explicit object key. For upload, defaults to prefix + basename(file). Required for verify. |
retention-days |
no | - | Delete objects under prefix older than N days (prune / upload). |
retention-count |
no | - | Keep only the newest N objects under prefix (prune / upload). |
verify-mode |
conditional | - | tar, zip, or sha256. Required for verify. |
verify-checksum |
conditional | - | Expected sha256 hex digest. Required when verify-mode: sha256. |
max-keys |
no | - | Maximum objects to return (list). |
endpoint-url |
no | - | Override R2 endpoint. Default: https://<account-id>.r2.cloudflarestorage.com. |
| Output | Op | Description |
|---|---|---|
object-key |
upload, verify | Full object key written/verified. |
size |
upload, verify | Size in bytes. |
pruned-count |
upload, prune | Number of objects deleted by retention. |
verified |
verify | true if integrity check passed. |
objects |
list | JSON array of {key, size, lastModified}. |
count |
list | Number of objects returned. |
total-size |
list | Total bytes across all listed objects. |
| Style | Tag | Behavior |
|---|---|---|
| Major | @v1 |
Auto-receives minor + patch updates (recommended) |
| Exact | @v1.0.0 |
Pinned, no automatic updates |
| SHA | @abc1234 |
Maximum reproducibility |
The reusable workflow's hardcoded internal uses: references match the same major version as the workflow file you call.
Create an R2 API token in the Cloudflare dashboard, scoped to the bucket(s) you want to back up to. Save:
CF_ACCOUNT_ID- your Cloudflare account IDR2_ACCESS_KEY_ID- the token's access key IDR2_SECRET_ACCESS_KEY- the token's secret access key
For the tunnel-ssh source mode you'll also need a Cloudflare Access service token; for ssh and tunnel-ssh modes you'll need an SSH_PRIVATE_KEY. See docs/source-modes.md for the full setup.
Ubuntu only - this action depends on the AWS CLI being pre-installed (it is on GitHub-hosted Ubuntu runners):
ubuntu-latest(Ubuntu 24.04)ubuntu-22.04ubuntu-20.04
For self-hosted runners, install AWS CLI v2 (with AWS_ENDPOINT_URL_S3 env var support, ~mid-2023 onward).
- Source modes -
local,ssh,tunnel-ssh: where the backup runs and how the archive reaches R2 - DB templates -
mysql,postgres,mongodb,sqlite: built-in dump recipes you can use instead of writing a custom backup-script - Notifications - Slack / Discord / generic-webhook pings on backup success or failure
- Architecture - design rationale, password traversal model, archive layout per engine, comparison with hand-rolled patterns
- Roadmap - planned features
- Changelog - version history
- Security - vulnerability reporting and security practices
- Contributing - how to contribute
- Examples - complete caller workflows for common cases
This action collects no data. No telemetry, no analytics, no external calls. Network traffic from this action goes only to:
- The Cloudflare R2 endpoint (
https://<account-id>.r2.cloudflarestorage.comor your override) - For
tunnel-sshsource mode: Cloudflare Access endpoints, via the wrapped action
The source is fully open - read every line in action.yml, scripts/, and templates/.
Apache 2.0 - (c) 2026 NX1X