# Backups from CI

> A nightly archive to your bucket from a workflow, with secrets from the runner and no file to check in. How to build it with s3nd, what to watch, and the full guide.

Canonical: https://www.s3nd.sh/use-cases/ci-backups · Markdown: https://www.s3nd.sh/use-cases/ci-backups.md · Français: https://www.s3nd.sh/fr/use-cases/ci-backups

A nightly archive to your bucket from a workflow, with secrets from the runner and no file to check in.

```yaml
- run: tar cz ./data | npx @s3nd/cli put - --name backup.tar.gz --expires-in never
  env:
    S3ND_BUCKET: transfers
    S3ND_PREFIX: backups
    S3ND_ENDPOINT: https://${{ secrets.R2_ACCOUNT_ID }}.r2.cloudflarestorage.com
    S3ND_REGION: auto
    AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
    AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
```

## What is actually going on.
### The problem
A workflow produces something worth keeping: a database dump, a build, a generated report. It needs to land in your bucket every night, without a config file in the repository and without a client library in the workflow.

### What s3nd does about it
npx @s3nd/cli put, with the bucket and credentials in environment variables from the runner’s secret store. Two flags turn a transfer into a backup: --expires-in never, and a prefix of its own so the lifecycle rule on transfers cannot reach it.

## The things that are easy to get wrong.
- **--json for the logs** — Machine-readable output, and s3nd doctor --json exits non-zero when a check fails, so it doubles as a deployment smoke test.
- **A prefix per policy** — Transfers expire, backups do not. Keeping them under different prefixes is what lets one bucket hold both.

Packages: @s3nd/cli. [The CLI reference](https://doc.s3nd.sh/docs/cli)
