# S3 storage providers

> Set up Wasabi, Amazon S3, Cloudflare R2, Backblaze B2, Spaces, Google Cloud or MinIO for ZoPanel backups, with a bucket-only key and the exact endpoint to enter.

Source: https://zopanel.net/docs/s3-providers  
Updated: 2026-10-09

ZoPanel can copy backups to any S3-compatible storage. This page explains what to create at each provider, which values to type into the panel, the smallest set of permissions the key needs, and how to fix the errors you are most likely to see.

## What ZoPanel stores in S3

| Feature | Where to set it up | What goes to the bucket |
| --- | --- | --- |
| [Remote backups](/docs/backups#remote-backups-to-s3) | **Settings → Remote backups**, **Where to**: **S3** | Every account backup and database backup, encrypted on the server first, plus the daily configuration backup (`.zpb`) |
| [Incremental backups (restic)](/docs/backups#incremental-backups-restic) | **Backups** page, **Incremental backups** card, **Destination**: S3 | One restic repository per server, encrypted by restic |
| [Customers' own storage](/docs/backups#customers-own-schedules-and-s3-storage) | The customer's **Backups** page, **Automatic backups** card, **Copy to my storage** | That customer's scheduled backups |

**Licensing.** Remote backups need a Pro license (see [Licensing](/docs/licensing)). Incremental backups do not have their own S3 form: when you choose S3 as their **Destination**, they use the endpoint, bucket and keys saved in **Settings → Remote backups**, so fill in and test that form first.

**Where the files go.** With the default **Folder prefix** `zopanel`, a server called `web1` writes:

| Path in the bucket | Contents |
| --- | --- |
| `zopanel/web1/<account>/` | Account backups, encrypted before upload (the name stays `.tar.gz`; the content starts with `ZPENC1`) |
| `zopanel/web1/<account>/db/` | Database backups |
| `zopanel/web1/_panel/` | Configuration backups (`.zpb`) |
| `zopanel/web1/restic/` | The incremental (restic) repository |
| `zopanel/web1/_zopanel/` | A small test file, written and deleted by **Test connection** |

Several servers can share one bucket, since each one writes under its own hostname. A separate key per server is still safer: see below.

## Before you start: the bucket and the key

**Create a dedicated bucket.** Use it for ZoPanel only. ZoPanel accepts bucket names of 3 to 63 characters: lowercase letters, digits, dots and hyphens. Underscores and capital letters are refused with "invalid bucket name". Pick a region close to the server for faster uploads, or in another country if you want the copies far from the data center.

**Create a key that can only reach this bucket.** Never use the account's root or master key. The key ZoPanel needs must be able to:

- check that the bucket exists (HEAD bucket, which needs the list permission on most providers),
- list objects,
- upload objects (large files are sent in 64 MB parts),
- download objects,
- delete objects (old copies beyond **Remote copies to keep per account**, the connection test file, and restic's pruning).

It does not need to create or delete buckets, or to see other buckets.

**Versioning and object lock.** ZoPanel removes old copies itself. If versioning is on, a removed copy becomes a "noncurrent version" and you keep paying for it: add a lifecycle rule that deletes noncurrent versions after a few days. Object lock (immutability) protects copies from someone who steals the key, but ZoPanel's own deletions then only add delete markers, and storage grows until the lock period ends. If you use object lock, keep the lock period short (for example 7 to 30 days) and pair it with a lifecycle rule for noncurrent versions.

**Cost tips.**

- **Minimum storage duration.** Some providers bill each object for a minimum time even if it is deleted sooner: Wasabi bills 90 days on its pay-as-you-go plan, and Cloudflare R2 bills 30 days for its Infrequent Access class (Standard has no minimum). Keep backups in the provider's standard class, and on Wasabi prefer a retention that keeps copies for at least 90 days.
- **Egress.** Restores download data, and every Saturday the incremental repository check reads back 2% of it. Most providers bill downloads. Cloudflare R2 has no egress fees.
- **Deduplication.** Incremental (restic) snapshots only upload what changed. A full account backup is a complete archive every time, so daily full backups with a long remote retention grow fast.

## The ZoPanel form

**Settings → Remote backups**, with **Where to** set to **S3**:

| Field | What to enter |
| --- | --- |
| **Provider presets** | Fills **Endpoint**, **Region** and **Path-style URLs** for AWS S3, Cloudflare R2, Backblaze B2, Wasabi or DigitalOcean. Then correct the region for your bucket. |
| **Endpoint** | Host name only, such as `s3.ap-southeast-1.wasabisys.com`. `https://` is optional. A port is allowed (`minio.example.com:9000`), a path such as `/bucket` is not. |
| **Region** | The bucket's region. Always fill it in: a wrong region is the most common cause of errors. |
| **Bucket** | The bucket name |
| **Folder prefix** | `zopanel` by default. Use the same value on a replacement server. |
| **Access key**, **Secret key** | The bucket-only key. The secret is shown as `********` after saving. It must be typed again when you change the endpoint, bucket or access key. |
| **Remote copies to keep per account** | 1 to 365 (default 14) |
| **Path-style URLs (R2, MinIO)** | Sends requests as `https://endpoint/bucket/...` instead of `https://bucket.endpoint/...`. Leave off for Amazon S3. ZoPanel already uses path-style for endpoints other than Amazon and Google, so turning it on is harmless for the others. |
| **Plain HTTP (private MinIO only)** | Only for a MinIO server on a private network. Keys and data then travel unencrypted. |

**Save** with the switch on runs the connection test, and does not save if it fails.

## Wasabi

> **Tested with ZoPanel** (October 2026, Wasabi us-east-1): connection test, account and database backups (uploaded encrypted), retention, restoring a deleted account from **Browse S3**, downloading a backup back to the server, incremental (restic) snapshots with a single-file and a database restore, and configuration backups.

1. **Create the bucket.** In the Wasabi console, click **Buckets → Create Bucket**, enter a name and choose a region.
2. **Create a policy.** Click **Policies → Create Policy** and paste the policy below, with your bucket name:

   ```json
   {
     "Version": "2012-10-17",
     "Statement": [
       {
         "Effect": "Allow",
         "Action": ["s3:ListBucket", "s3:GetBucketLocation"],
         "Resource": "arn:aws:s3:::zopanel-backups"
       },
       {
         "Effect": "Allow",
         "Action": ["s3:GetObject", "s3:PutObject", "s3:DeleteObject", "s3:AbortMultipartUpload"],
         "Resource": "arn:aws:s3:::zopanel-backups/*"
       }
     ]
   }
   ```

   Wasabi's own bucket-separation example grants `s3:*` on the bucket and its objects, plus `s3:ListAllMyBuckets` so the user can see buckets in the console. A key used only by ZoPanel does not need console access.
3. **Create a sub-user.** Click **Users → Create User**, enter a name and tick **Programmatic** access only. Skip the group step, attach the policy from step 2 instead of **WasabiFullAccess**, and click **Create User**. Copy the access key and the secret key it shows.
4. **In ZoPanel**, click the **Wasabi** preset, then set:

   | Field | Value |
   | --- | --- |
   | Endpoint | `s3.<region>.wasabisys.com`, for example `s3.ap-southeast-1.wasabisys.com`, `s3.eu-central-1.wasabisys.com` |
   | Region | The same region, for example `ap-southeast-1` |
   | Path-style URLs | Off |

   For **US East 1**, the endpoint is `s3.wasabisys.com` (or its alias `s3.us-east-1.wasabisys.com`) with region `us-east-1`. Use the endpoint of the region where the bucket was created.

**Watch out:** on pay-as-you-go, Wasabi bills every object for at least 90 days. With daily backups and the default 14 copies, each deleted copy is still billed for the remaining 76 days. Setting **Remote copies to keep per account** to 90 costs about the same and gives you three months of restore points.

## Amazon S3

1. **Create the bucket.** In the S3 console, create a general purpose bucket in the region you want. Leave **Block all public access** on.
2. **Create the policy.** In IAM, create a policy with this JSON (replace the bucket name):

   ```json
   {
     "Version": "2012-10-17",
     "Statement": [
       {
         "Sid": "Bucket",
         "Effect": "Allow",
         "Action": ["s3:ListBucket", "s3:GetBucketLocation"],
         "Resource": "arn:aws:s3:::zopanel-backups"
       },
       {
         "Sid": "Objects",
         "Effect": "Allow",
         "Action": ["s3:GetObject", "s3:PutObject", "s3:DeleteObject", "s3:AbortMultipartUpload"],
         "Resource": "arn:aws:s3:::zopanel-backups/*"
       }
     ]
   }
   ```

   The bucket ARN (without `/*`) is for listing, the `/*` ARN for the objects. Mixing them up gives AccessDenied.
3. **Create an IAM user** without console access, and attach only this policy.
4. **Create an access key.** Open the user, then the **Security credentials** tab, and click **Create access key**. Choose **Other** as the use case, then **Create access key**. The secret access key is shown only on this page: copy it or download the `.csv` file.
5. **In ZoPanel**, click **AWS S3**, then set:

   | Field | Value |
   | --- | --- |
   | Endpoint | `s3.<region>.amazonaws.com`, for example `s3.ap-southeast-1.amazonaws.com` (Singapore), `s3.eu-central-1.amazonaws.com` (Frankfurt), `s3.us-east-1.amazonaws.com` |
   | Region | The bucket's region, for example `ap-southeast-1` |
   | Path-style URLs | Off |

   The preset fills `s3.amazonaws.com` with region `ap-southeast-1`: correct the region if your bucket is elsewhere, or use the regional endpoint.

**Watch out:** new buckets store objects in S3 Standard, which has no minimum storage duration. A lifecycle rule that moves backups to Standard-IA or Glacier adds minimum durations and retrieval fees, and Glacier objects cannot be read until they are restored, so ZoPanel cannot restore from them directly.

## Cloudflare R2

1. **Create the bucket.** In the Cloudflare dashboard, open **R2 object storage** and create a bucket. Keep the **Standard** storage class.
2. **Create an API token.** On the **R2 object storage** overview, under **Account Details**, click **Manage** next to **API Tokens**, then **Create Account API token**.
   - **Permissions:** **Object Read & Write**.
   - Limit the token to your bucket. Object permissions can be scoped to specific buckets.
   - Click **Create Account API token**. Copy the **Access Key ID** and **Secret Access Key**: the secret cannot be shown again.
3. **Find your account ID.** It is shown in the Cloudflare dashboard.
4. **In ZoPanel**, click **Cloudflare R2**, then set:

   | Field | Value |
   | --- | --- |
   | Endpoint | `<ACCOUNT_ID>.r2.cloudflarestorage.com` |
   | Region | `auto` |
   | Path-style URLs | On (set by the preset) |

   A bucket created in the EU jurisdiction uses `<ACCOUNT_ID>.eu.r2.cloudflarestorage.com`.

**Watch out:** enter the endpoint without the bucket name. If you paste an address ending in `/<bucket>`, ZoPanel answers "invalid S3 endpoint". R2 does not charge for egress, which makes restores and the weekly restic check free of transfer costs.

## Backblaze B2

1. **Create the bucket.** In the Backblaze web console, create a **Private** bucket. Note the **Endpoint** shown for it, for example `s3.us-west-004.backblazeb2.com`.
2. **Set the lifecycle.** B2 buckets keep file versions by default. In the bucket's lifecycle settings, choose **Keep only the last version** so older versions do not keep using space.
3. **Create an application key.** Under **B2 Cloud Storage → Application Keys**, click **Add a New Application Key**:
   - **Allow access to Bucket(s):** your bucket only.
   - **Type of Access:** **Read and Write**.
   - Tick **Allow List All Bucket Names**. Bucket-restricted keys need it for S3 tools.
   - Click **Create New Key**, then copy the **keyID** and **applicationKey**. The applicationKey is shown only once.

   The master application key does not work with the S3-compatible API.
4. **In ZoPanel**, click **Backblaze B2**, then set:

   | Field | Value |
   | --- | --- |
   | Endpoint | `s3.<region>.backblazeb2.com`, as shown on the bucket, for example `s3.us-west-004.backblazeb2.com` |
   | Region | The middle part of the endpoint, for example `us-west-004` |
   | Access key / Secret key | keyID / applicationKey |

   The preset fills `us-west-004`. Your account may be in another region: always copy the endpoint from your bucket.

**S3 or "Backblaze B2"?** **Where to** also offers a native **Backblaze B2** target. It works for remote backups, but incremental backups need the **S3** form. Use the S3-compatible endpoint above if you want both.

## DigitalOcean Spaces

1. **Create the bucket** in **Spaces Object Storage**, in a datacenter region such as `sgp1`, `nyc3` or `fra1`.
2. **Create a limited key.** On the **Access Keys** tab, click **Create Access Key**:
   - **Select access scope:** **Limited access**.
   - Select your bucket and set its **Permissions** to **Read/Write/Delete**.
   - Name the key and click **Create Access Key**. The secret key appears only once.
3. **In ZoPanel**, click **DigitalOcean**, then set:

   | Field | Value |
   | --- | --- |
   | Endpoint | `<region>.digitaloceanspaces.com`, for example `sgp1.digitaloceanspaces.com` |
   | Region | The region slug, for example `sgp1` |

**Watch out:** use the regional endpoint, not the bucket's own address (`<bucket>.<region>.digitaloceanspaces.com`). Limited access keys cannot be combined with bucket policies on the same bucket.

## Google Cloud Storage

Cloud Storage accepts S3-style requests through its XML API with **HMAC keys**.

1. **Create the bucket** in Cloud Storage, with the Standard storage class and uniform access.
2. **Create a service account** (IAM & Admin → Service accounts) with no project roles.
3. **Give it access to the bucket only.** On the bucket's **Permissions** tab, grant the service account:
   - **Storage Object Admin** (`roles/storage.objectAdmin`): read, write, delete and multipart uploads,
   - **Storage Legacy Bucket Reader** (`roles/storage.legacyBucketReader`): adds `storage.buckets.get`, which the connection test needs to check that the bucket exists.
4. **Create the HMAC key.** Open Cloud Storage **Settings**, the **Interoperability** tab, and click **Create a key for a service account**. Choose the service account and click **Create key**. Store the secret now: it cannot be recovered later.
5. **In ZoPanel** (no preset), set:

   | Field | Value |
   | --- | --- |
   | Endpoint | `storage.googleapis.com` |
   | Region | The bucket's location, for example `asia-southeast1`, or `auto` |
   | Access key / Secret key | The HMAC access ID / secret |
   | Path-style URLs | Off |

## MinIO and other self-hosted storage

1. **Create the bucket and a user** with the MinIO client. Save the policy below as `zopanel.json`:

   ```json
   {
     "Version": "2012-10-17",
     "Statement": [
       {
         "Effect": "Allow",
         "Action": ["s3:ListBucket", "s3:GetBucketLocation"],
         "Resource": ["arn:aws:s3:::zopanel-backups"]
       },
       {
         "Effect": "Allow",
         "Action": ["s3:GetObject", "s3:PutObject", "s3:DeleteObject", "s3:AbortMultipartUpload"],
         "Resource": ["arn:aws:s3:::zopanel-backups/*"]
       }
     ]
   }
   ```

   ```bash
   mc mb myminio/zopanel-backups
   mc admin user add myminio zopanel-web1 '<a long random secret>'
   mc admin policy create myminio zopanel-backups zopanel.json
   mc admin policy attach myminio zopanel-backups --user zopanel-web1
   ```

2. **In ZoPanel** (no preset), set:

   | Field | Value |
   | --- | --- |
   | Endpoint | `minio.example.com` or `minio.example.com:9000` |
   | Region | The server's region, `us-east-1` unless you changed it |
   | Path-style URLs | On |
   | Plain HTTP | Only when the MinIO server is on a private network without TLS |

**Watch out:** give MinIO a certificate from a public authority (for example Let's Encrypt). A self-signed certificate fails with a TLS error, because ZoPanel checks certificates and has no option to skip that check.

## ZoPanel S3 storage on another server

A second ZoPanel server with the [S3 storage](/docs/apps#s3-object-storage) component makes a cheap off-site target. Never use the S3 storage of the same server: it would be lost with it.

1. On the **storage server**, install **S3 storage** and publish it on a domain with **Publish on a domain** (for example `s3.example.com`).
2. Create a bucket in **S3 storage → New bucket**, from an account kept for backups. The bucket gets its own access key that works only for that bucket. The secret is shown once (use **Rotate key** if it is lost).
3. In the bucket's settings, leave **Delete objects after (days)** empty, or set it well beyond your retention. ZoPanel deletes old copies itself, and the incremental repository breaks if the storage deletes its files.
4. On the **server being backed up**, set:

   | Field | Value |
   | --- | --- |
   | Endpoint | `s3.example.com` |
   | Region | `us-east-1` |
   | Path-style URLs | On |

The bucket's size is limited by that account's package disk size: pick a package large enough for your retention.

## Test the connection and make a first backup

1. Click **Test connection**. ZoPanel checks that the bucket exists, writes a small file under `<prefix>/<hostname>/_zopanel/` and deletes it. "Connection works" means the key can list, write and delete.
2. Turn the switch on and click **Save**.
3. On the **Backups** page, choose an account and click **Create backup**. The job log shows `Uploading … (encrypted)` and then `Uploaded … bytes`. The file appears in the bucket under `<prefix>/<hostname>/<account>/`.
4. Open **Backups → Disaster recovery → Show key** and store the recovery key outside the server. Without it, nothing in the bucket can be read on another server.
5. For incremental backups, choose S3 as the **Destination** in the **Incremental backups** card and save. The job log shows the repository address (`s3:https://<endpoint>/<bucket>/<prefix>/<hostname>/restic`). Click **Show recovery key** and store that key too.

If you later change the endpoint, bucket or keys in **Settings → Remote backups**, save the **Incremental backups** card again: restic keeps the S3 settings it was given when it was set up.

## Restore from S3

- **One account:** **Backups → Disaster recovery → Browse S3**, open the server, then the account, and click **Restore** on a backup. See [Restoring a whole account](/docs/backups#a-whole-account).
- **Single files, folders, mail or databases** from the incremental repository: the **Snapshots (incremental)** card on the **Backups** page. See [Restoring](/docs/backups#restoring).
- **A whole server:** connect the same bucket and **Folder prefix** on a new server, restore the configuration backup from `_panel` with the recovery key, then the accounts. See [Server lost: restore on a new server](/docs/disaster-recovery#server-lost-restore-on-a-new-server).

Test a restore on a spare server once in a while. It also proves that the key and endpoint you wrote down still work.

## Troubleshooting

ZoPanel shows the storage provider's error after "cannot access bucket:", "write test failed:" or "upload failed:". The first check (does the bucket exist?) is a HEAD request, and providers send no error text for it. So a wrong secret, a wrong region, a skewed clock and a missing permission can all show as **cannot access bucket: Access Denied.** The full error code appears on the write test, in the upload job log or with any S3 tool using the same key.

| Error | Likely cause | Fix |
| --- | --- | --- |
| `SignatureDoesNotMatch` | Wrong secret key, a space copied with it, or a wrong region | Paste the secret again (the field keeps spaces). Check the **Region**. On B2, use the applicationKey, not the master key. |
| `AccessDenied` / `Access Denied.` | The key's policy lacks a permission, the policy is attached to a different bucket name, or the R2 token is scoped to another bucket | Compare the policy with the examples above: list on the bucket ARN, object actions on `bucket/*`. On B2, tick **Allow List All Bucket Names**. On Google Cloud, add **Storage Legacy Bucket Reader**. |
| `bucket does not exist` / `NoSuchBucket` | Bucket name misspelled, or the endpoint is in another region or account | Copy the bucket name exactly. Use the endpoint of the bucket's region (Wasabi, B2, Spaces). |
| `invalid bucket name` | Capital letters, underscores, or fewer than 3 characters | Create a bucket with a valid name |
| `invalid S3 endpoint` | The endpoint contains a path (`/bucket`) or other characters | Enter the host name only, with an optional `:port` |
| `the bucket is in region …` | The bucket was created in another region than the endpoint and region you entered (the Wasabi preset fills in `ap-southeast-1`) | Set **Region** to the region named, and use that region's endpoint, e.g. `s3.us-east-1.wasabisys.com`. |
| `301 Moved Permanently` | The endpoint belongs to another region than the bucket's | Use the endpoint of the bucket's region. |
| `400 Bad Request`, `AuthorizationHeaderMalformed`, `the region '…' is wrong; expecting '…'` | Region does not match the bucket | Set **Region** to the value the error expects. On R2 it is `auto`. |
| `RequestTimeTooSkewed`, "the difference between the request time and the current time is too large" | The server clock is off (S3 refuses requests more than about 15 minutes off) | Run `zopanel ctl doctor`, which checks the clock. Turn on NTP: `timedatectl set-ntp true`. |
| `no such host`, connection timeouts | Endpoint misspelled, or the firewall blocks outgoing HTTPS | Check the host name with `getent hosts <endpoint>`. Allow outgoing port 443 (or the MinIO port). |
| `no such host` or a certificate error naming `<bucket>.<endpoint>` | Virtual-host addressing: the bucket name is put in front of the endpoint, and that name does not exist | Turn **Path-style URLs** on (R2, MinIO, ZoPanel S3 storage and other self-hosted storage). Leave it off for Amazon S3. |
| `x509: certificate signed by unknown authority`, `certificate is valid for …, not …` | Self-signed certificate, or the endpoint name does not match the certificate | Use a publicly trusted certificate whose name matches the **Endpoint**. Only for MinIO on a private network, use **Plain HTTP**. |
| `re-enter the secret key when changing the endpoint, bucket or access key` | The saved secret is only reused for the same target | Type the secret key again |
| Incremental backups still write to the old bucket | restic kept the settings from when it was set up | Save the **Incremental backups** card again |
| Storage bill grows although old copies are deleted | Versioning, object lock or a minimum storage duration | Add a lifecycle rule for noncurrent versions. On B2, choose **Keep only the last version**. On Wasabi, see the 90-day note above. |

## Related

- [Backups](/docs/backups)
- [Disaster recovery](/docs/disaster-recovery)
- [Licensing](/docs/licensing)
- [App Store and S3 storage](/docs/apps#s3-object-storage)
- [Security](/docs/security)
- [Troubleshooting](/docs/troubleshooting)
