diff --git a/README.md b/README.md index dbdf84c0..3025baae 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ In order to run Zerobyte, you need to have Docker and Docker Compose installed o ```yaml services: zerobyte: - image: ghcr.io/nicotsx/zerobyte:v0.33 + image: ghcr.io/nicotsx/zerobyte:v0.34 container_name: zerobyte restart: unless-stopped cap_add: @@ -123,6 +123,8 @@ Provisioned resources: - appear in the normal repositories and volumes screens - can resolve credential fields from environment variables or `/run/secrets/*` during startup sync +The complete provisioning documentation is available at [zerobyte.app/docs/guides/provisioning](https://zerobyte.app/docs/guides/provisioning). + See `examples/provisioned-resources/README.md` for a full example. ### Simplified setup (No remote mounts) @@ -132,7 +134,7 @@ If you only need to back up locally mounted folders and don't require remote sha ```yaml services: zerobyte: - image: ghcr.io/nicotsx/zerobyte:v0.33 + image: ghcr.io/nicotsx/zerobyte:v0.34 container_name: zerobyte restart: unless-stopped ports: @@ -171,7 +173,7 @@ If you want to track a local directory on the same server where Zerobyte is runn ```diff services: zerobyte: - image: ghcr.io/nicotsx/zerobyte:v0.33 + image: ghcr.io/nicotsx/zerobyte:v0.34 container_name: zerobyte restart: unless-stopped cap_add: @@ -246,7 +248,7 @@ Zerobyte can use [rclone](https://rclone.org/) to support 40+ cloud storage prov ```diff services: zerobyte: - image: ghcr.io/nicotsx/zerobyte:v0.33 + image: ghcr.io/nicotsx/zerobyte:v0.34 container_name: zerobyte restart: unless-stopped cap_add: diff --git a/apps/docs/content/docs/guides/provisioning.mdx b/apps/docs/content/docs/guides/provisioning.mdx index 7915f06d..99010cc6 100644 --- a/apps/docs/content/docs/guides/provisioning.mdx +++ b/apps/docs/content/docs/guides/provisioning.mdx @@ -3,23 +3,51 @@ title: Provisioned Resources description: Manage repositories and volumes through a configuration file with secret references --- -Zerobyte can sync operator-managed repositories and volumes from a JSON configuration file at startup. This is useful when you want credentials and connection details to live in deployment-time configuration (environment variables, Docker secrets) instead of being entered through the UI. +import { Step, Steps } from "fumadocs-ui/components/steps"; +import { Callout } from "fumadocs-ui/components/callout"; -## Why Use Provisioning? +Zerobyte can sync operator-managed repositories and volumes from a JSON configuration file at startup. This is useful when you want credentials and connection details to live in deployment-time configuration instead of being entered through the UI. -- **Infrastructure as code**, define repositories and volumes in version-controlled config files -- **Secret management**, keep credentials in environment variables or Docker secrets, not in the UI -- **Easy rotation**, rotate secrets by updating env vars or secret files and restarting -- **Consistent deployments**, use the same configuration across staging and production +## What Provisioning Supports Today -Provisioned resources appear in the normal UI alongside manually created ones, marked as managed entries. +Provisioning currently supports: + +- repositories +- volumes +- secret references through `env://` and `file://` +- updating managed resources in place by keeping the same `id` +- deleting managed resources with `"delete": true` + +Provisioning does **not** currently support: + +- backup jobs or schedules +- notification destinations +- exporting UI-created resources back into provisioning JSON +- live reloading without restarting the Zerobyte container + + + Provisioning is limited to repositories and volumes in schema version `1`. If you need to manage backup jobs or + notifications, create them through the UI or API for now. + ## Prerequisites -- A running Zerobyte instance with an organization ID (found in Settings after first-run setup) +- A running Zerobyte instance with an organization ID - The `PROVISIONING_PATH` environment variable pointing to your JSON file -## Setup +### Find your organization ID + +1. Sign in to Zerobyte and switch to the organization you want to provision into +2. Open **Settings** +3. Open the **Organization** tab +4. Copy the read-only **Organization ID** value from **Organization Details** + + + The **Organization** tab is only visible to organization `admin` and `owner` members. If you do not see it, ask an + organization admin or owner for the ID, or have them grant you the required role first. + + +## Quick Start @@ -30,49 +58,43 @@ Create a `provisioning.json` file: ```json provisioning.json { - "version": 1, - "repositories": [ - { - "id": "aws-prod", - "organizationId": "your-organization-id", - "name": "AWS Production Backups", - "backend": "s3", - "compressionMode": "auto", - "config": { - "backend": "s3", - "endpoint": "https://s3.amazonaws.com", - "bucket": "company-backups", - "accessKeyId": "env://AWS_ACCESS_KEY_ID", - "secretAccessKey": "file://aws_secret_access_key" - } - } - ], - "volumes": [ - { - "id": "webdav-team-a", - "organizationId": "your-organization-id", - "name": "Team A WebDAV", - "backend": "webdav", - "config": { - "backend": "webdav", - "server": "cloud.example.com", - "path": "/team-a", - "username": "team-a", - "password": "env://WEBDAV_PASSWORD", - "port": 443, - "ssl": true - } - } - ] + "version": 1, + "repositories": [ + { + "id": "local-repo", + "organizationId": "your-organization-id", + "name": "Primary Local Repository", + "backend": "local", + "compressionMode": "auto", + "config": { + "backend": "local", + "path": "/var/lib/zerobyte/repositories/primary", + "isExistingRepository": false + } + } + ], + "volumes": [ + { + "id": "documents", + "organizationId": "your-organization-id", + "name": "Documents", + "backend": "directory", + "autoRemount": true, + "config": { + "backend": "directory", + "path": "/data/documents" + } + } + ] } ``` -### Configure docker-compose.yml +### Mount the file and configure Zerobyte -Mount the provisioning file and set the environment variable: +Mount the provisioning file and set `PROVISIONING_PATH`: ```yaml docker-compose.yml services: @@ -80,17 +102,10 @@ services: image: ghcr.io/nicotsx/zerobyte:latest environment: - PROVISIONING_PATH=/config/provisioning.json - - AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE - - WEBDAV_PASSWORD=your-webdav-password volumes: - ./provisioning.json:/config/provisioning.json:ro - /var/lib/zerobyte:/var/lib/zerobyte - secrets: - - aws_secret_access_key - -secrets: - aws_secret_access_key: - file: ./secrets/aws_secret_access_key + - /srv/documents:/data/documents ``` @@ -102,55 +117,509 @@ secrets: docker compose up -d ``` -On startup, Zerobyte reads the provisioning file, resolves all secret references, encrypts the resolved values, and syncs the resources into the database. +On startup, Zerobyte reads the provisioning file, resolves supported secret references, encrypts the resolved secret values, and syncs the managed resources into the database. -## Secret References +## Root File Format -Provisioned resources support two types of secret references for sensitive fields (passwords, access keys, etc.): - -| Reference | Resolves From | Example | -|-----------|--------------|---------| -| `env://VARIABLE_NAME` | Container environment variable | `env://AWS_ACCESS_KEY_ID` | -| `file://secret_name` | `/run/secrets/secret_name` (Docker secrets) | `file://aws_secret_access_key` | - - - `file://` references always resolve from `/run/secrets/` and must be a single filename, not a nested path. - - -Resolved values are encrypted before Zerobyte stores them in the database, and the plaintext never persists on disk. - -## Rotating Secrets - -To rotate a secret: - -1. Update the environment variable or secret file with the new value -2. Restart the Zerobyte container: `docker compose restart` - -Zerobyte re-resolves all secret references on each startup. - -## Removing Provisioned Resources - -To remove a provisioned resource, add `"delete": true` to the entry in your provisioning file, then restart: +The root object always has the same shape: ```json { - "id": "aws-prod", - "delete": true + "version": 1, + "repositories": [], + "volumes": [] } ``` +| Field | Required | Notes | +| -------------- | -------- | --------------------------------------------------- | +| `version` | yes | Must be `1` | +| `repositories` | no | Array of provisioned repositories. Defaults to `[]` | +| `volumes` | no | Array of provisioned volumes. Defaults to `[]` | + +## Repository Entry Reference + +Each item in `repositories` uses this top-level shape: + +```json +{ + "id": "repo-id", + "organizationId": "org-id", + "name": "Repository Name", + "backend": "local", + "compressionMode": "auto", + "delete": false, + "config": { + "backend": "local", + "path": "/var/lib/zerobyte/repositories/repo-id" + } +} +``` + +| Field | Required | Notes | +| ----------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------- | +| `id` | yes | Stable provisioning identifier inside the organization. Keep this stable if you want Zerobyte to update the same resource in place | +| `organizationId` | yes | Must match an existing organization | +| `name` | yes | Display name in Zerobyte | +| `backend` | yes | Must be one of `local`, `s3`, `r2`, `gcs`, `azure`, `rclone`, `rest`, `sftp` | +| `compressionMode` | no | `off`, `auto`, or `max` | +| `delete` | no | Defaults to `false` | +| `config` | yes | Backend-specific object. `config.backend` must match the top-level `backend` value | + +### Repository Shared Config Fields + +These fields are available inside every repository `config` object: + +| Field | Required | Notes | +| ---------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `isExistingRepository` | no | When `true`, Zerobyte treats the repository as already initialized. When omitted or `false`, a newly created provisioned repository is initialized with `restic init` on first sync | +| `customPassword` | no | Overrides the organization-level repository password | +| `cacert` | no | Custom CA certificate contents | +| `insecureTls` | no | Disables TLS verification for supported backends | +| `uploadLimit` | no | Bandwidth limit object | +| `downloadLimit` | no | Bandwidth limit object | + +Bandwidth limit objects use this shape: + +```json +{ + "enabled": true, + "value": 10, + "unit": "Mbps" +} +``` + +`unit` must be one of `Kbps`, `Mbps`, or `Gbps`. + +### Repository Backends + + + + | Field | Required | Notes | + | ------ | -------- | ---------------------------------- | + | `path` | yes | Path inside the Zerobyte container | + + ```json + { + "backend": "local", + "path": "/var/lib/zerobyte/repositories/primary", + "isExistingRepository": false + } + ``` + + + + + | Field | Required | Notes | + | ----------------- | -------- | ----------------------------------- | + | `endpoint` | yes | Example: `https://s3.amazonaws.com` | + | `bucket` | yes | Bucket name | + | `accessKeyId` | yes | Supports secret references | + | `secretAccessKey` | yes | Supports secret references | + + ```json + { + "backend": "s3", + "endpoint": "https://s3.amazonaws.com", + "bucket": "company-backups", + "accessKeyId": "env://AWS_ACCESS_KEY_ID", + "secretAccessKey": "file://aws_secret_access_key", + "isExistingRepository": true + } + ``` + + + + + | Field | Required | Notes | + | ----------------- | -------- | ------------------------------ | + | `endpoint` | yes | Your Cloudflare R2 S3 endpoint | + | `bucket` | yes | Bucket name | + | `accessKeyId` | yes | Supports secret references | + | `secretAccessKey` | yes | Supports secret references | + + ```json + { + "backend": "r2", + "endpoint": "https://.r2.cloudflarestorage.com", + "bucket": "zerobyte-backups", + "accessKeyId": "env://R2_ACCESS_KEY_ID", + "secretAccessKey": "env://R2_SECRET_ACCESS_KEY", + "isExistingRepository": true + } + ``` + + + + + | Field | Required | Notes | + | ----------------- | -------- | ------------------------------------------------ | + | `bucket` | yes | Bucket name | + | `projectId` | yes | Google Cloud project ID | + | `credentialsJson` | yes | Service account JSON. Supports secret references | + + ```json + { + "backend": "gcs", + "bucket": "zerobyte-backups", + "projectId": "my-gcp-project", + "credentialsJson": "file://gcs_credentials_json", + "isExistingRepository": true + } + ``` + + + + + | Field | Required | Notes | + | ---------------- | -------- | -------------------------- | + | `container` | yes | Blob container name | + | `accountName` | yes | Storage account name | + | `accountKey` | yes | Supports secret references | + | `endpointSuffix` | no | Custom endpoint suffix | + + ```json + { + "backend": "azure", + "container": "zerobyte-backups", + "accountName": "storageaccount", + "accountKey": "env://AZURE_STORAGE_ACCOUNT_KEY", + "endpointSuffix": "core.windows.net", + "isExistingRepository": true + } + ``` + + + + + | Field | Required | Notes | + | -------- | -------- | ------------------------------------ | + | `remote` | yes | Name of the configured rclone remote | + | `path` | yes | Path inside that remote | + + ```json + { + "backend": "rclone", + "remote": "remote-name", + "path": "zerobyte/backups", + "isExistingRepository": true + } + ``` + + + + + | Field | Required | Notes | + | ---------- | -------- | ------------------------------------- | + | `url` | yes | Base REST server URL | + | `username` | no | Supports secret references | + | `password` | no | Supports secret references | + | `path` | no | Optional path below the REST endpoint | + + ```json + { + "backend": "rest", + "url": "https://rest-server.example.com", + "username": "env://REST_USERNAME", + "password": "file://rest_password", + "path": "zerobyte", + "isExistingRepository": true + } + ``` + + + + + | Field | Required | Notes | + | ------------------ | -------- | ------------------------------------------------ | + | `host` | yes | SFTP hostname | + | `port` | no | Defaults to `22` | + | `user` | yes | SSH username | + | `path` | yes | Remote repository path | + | `privateKey` | yes | Private key contents. Supports secret references | + | `skipHostKeyCheck` | no | Defaults to `false` | + | `knownHosts` | no | Contents of a known hosts file | + + ```json + { + "backend": "sftp", + "host": "backup.example.com", + "port": 22, + "user": "backup", + "path": "/srv/restic", + "privateKey": "file://sftp_private_key", + "skipHostKeyCheck": false, + "knownHosts": "backup.example.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI..." + } + ``` + + + + +## Volume Entry Reference + +Each item in `volumes` uses this top-level shape: + +```json +{ + "id": "volume-id", + "organizationId": "org-id", + "name": "Volume Name", + "backend": "directory", + "autoRemount": true, + "delete": false, + "config": { + "backend": "directory", + "path": "/data/volume" + } +} +``` + +| Field | Required | Notes | +| ---------------- | -------- | ---------------------------------------------------------------------------------- | +| `id` | yes | Stable provisioning identifier inside the organization | +| `organizationId` | yes | Must match an existing organization | +| `name` | yes | Display name in Zerobyte | +| `backend` | yes | Must be one of `nfs`, `smb`, `directory`, `webdav`, `rclone`, `sftp` | +| `autoRemount` | no | Defaults to `true` | +| `delete` | no | Defaults to `false` | +| `config` | yes | Backend-specific object. `config.backend` must match the top-level `backend` value | + +### Volume Backends + + + + | Field | Required | Notes | + | ---------- | -------- | ---------------------------------- | + | `path` | yes | Path inside the Zerobyte container | + | `readOnly` | no | If provided, it must be `false` | + + ```json + { + "backend": "directory", + "path": "/data/documents" + } + ``` + + + + + | Field | Required | Notes | + | ------------ | -------- | --------------------------- | + | `server` | yes | NFS server hostname or IP | + | `exportPath` | yes | Exported path on the server | + | `port` | no | Defaults to `2049` | + | `version` | yes | Must be `3`, `4`, or `4.1` | + | `readOnly` | no | Mount the volume read-only | + + ```json + { + "backend": "nfs", + "server": "10.0.0.10", + "exportPath": "/exports/media", + "port": 2049, + "version": "4.1", + "readOnly": true + } + ``` + + + + + | Field | Required | Notes | + | ---------- | -------- | --------------------------------------------------------- | + | `server` | yes | SMB server hostname or IP | + | `share` | yes | Share name | + | `username` | no | Login username | + | `password` | no | Supports secret references | + | `guest` | no | Use guest authentication | + | `vers` | no | `1.0`, `2.0`, `2.1`, `3.0`, or `auto`. Defaults to `auto` | + | `domain` | no | SMB domain or workgroup | + | `port` | no | Defaults to `445` | + | `readOnly` | no | Mount the volume read-only | + + ```json + { + "backend": "smb", + "server": "fileserver.local", + "share": "team", + "username": "backup-user", + "password": "env://SMB_PASSWORD", + "vers": "3.0", + "domain": "WORKGROUP", + "port": 445, + "readOnly": false + } + ``` + + + + + | Field | Required | Notes | + | ---------- | -------- | ------------------------------- | + | `server` | yes | WebDAV hostname, without scheme | + | `path` | yes | Remote path | + | `username` | no | Login username | + | `password` | no | Supports secret references | + | `port` | no | Defaults to `80` | + | `readOnly` | no | Mount the volume read-only | + | `ssl` | no | Enable HTTPS | + + ```json + { + "backend": "webdav", + "server": "cloud.example.com", + "path": "/team-a", + "username": "team-a", + "password": "env://WEBDAV_PASSWORD", + "port": 443, + "ssl": true + } + ``` + + + + + | Field | Required | Notes | + | ------------------ | -------- | ------------------------------------------------ | + | `host` | yes | SFTP hostname | + | `port` | no | Defaults to `22` | + | `username` | yes | SSH username | + | `password` | no | Supports secret references | + | `privateKey` | no | Private key contents. Supports secret references | + | `path` | yes | Remote path | + | `readOnly` | no | Mount the volume read-only | + | `skipHostKeyCheck` | no | Defaults to `false` | + | `knownHosts` | no | Contents of a known hosts file | + + ```json + { + "backend": "sftp", + "host": "files.example.com", + "port": 22, + "username": "backup-user", + "privateKey": "file://volume_sftp_private_key", + "path": "/srv/data", + "readOnly": true, + "skipHostKeyCheck": false, + "knownHosts": "files.example.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI..." + } + ``` + + + + + | Field | Required | Notes | + | ---------- | -------- | ------------------------------------ | + | `remote` | yes | Name of the configured rclone remote | + | `path` | yes | Path inside that remote | + | `readOnly` | no | Mount the volume read-only | + + ```json + { + "backend": "rclone", + "remote": "remote-name", + "path": "team-a", + "readOnly": true + } + ``` + + + + +## Secret References + +Supported secret references use one of these prefixes: + +| Reference | Resolves From | Example | +| --------------------- | ------------------------------- | ------------------------------ | +| `env://VARIABLE_NAME` | Container environment variables | `env://AWS_ACCESS_KEY_ID` | +| `file://secret_name` | `/run/secrets/secret_name` | `file://aws_secret_access_key` | + + + `file://` references always resolve from `/run/secrets/` and must be a single filename, not a nested path. + + +Only specific fields are resolved as secret references. Other string fields are treated literally. + +### Repository fields that support secret references + +- shared fields: `customPassword`, `cacert` +- `s3`: `accessKeyId`, `secretAccessKey` +- `r2`: `accessKeyId`, `secretAccessKey` +- `gcs`: `credentialsJson` +- `azure`: `accountKey` +- `rest`: `username`, `password` +- `sftp`: `privateKey` + +### Volume fields that support secret references + +- `smb`: `password` +- `webdav`: `password` +- `sftp`: `password`, `privateKey` + +Resolved secret values are encrypted before Zerobyte stores them in the database. + +## Rotating Secrets + +To rotate a provisioned secret: + +1. Update the environment variable or secret file +2. Restart Zerobyte with `docker compose restart` + +Zerobyte re-resolves supported secret references on each startup. + +## Updating And Removing Managed Resources + +- Keep the same `id` to update a managed resource in place +- Changing only `name` keeps the same underlying resource record +- Setting `"delete": true` removes the managed resource on the next startup sync + +Deletion entries still need the normal entry shape today because the provisioning file is validated before Zerobyte applies the `delete` flag: + +```json +{ + "id": "repo-to-remove", + "organizationId": "your-organization-id", + "name": "Repo to remove", + "backend": "local", + "delete": true, + "config": { + "backend": "local", + "path": "/var/lib/zerobyte/repositories/old", + "isExistingRepository": true + } +} +``` + +## Troubleshooting + +### `No matching discriminator` + +This error usually means one of these is wrong: + +- `backend` is not a valid value for that resource type +- `config.backend` does not match the top-level `backend` +- the `config` object does not match the required fields for that backend + +### Secret reference errors + +- `env://NAME` requires the environment variable to exist in the Zerobyte container +- `file://name` requires `/run/secrets/name` to exist +- Zerobyte stops syncing when it hits a provisioning error, so fix the first reported error and restart again + ## Important Notes - Each provisioned entry must reference an existing `organizationId`. Complete first-run setup before enabling provisioning so you have a valid organization ID. + Each provisioned entry must reference an existing `organizationId`. Complete first-run setup before enabling + provisioning so you have a valid organization ID. - Changes to `provisioning.json` only apply on container restart -- Each entry needs both a top-level `backend` and the matching `config.backend` field -- Provisioned resources can be viewed in the UI but credential fields managed by provisioning will be re-synced from the config file on restart +- Provisioned resources appear in the normal UI and are marked as managed +- Editing a provisioned resource in the UI is useful for testing, but the next provisioning sync can overwrite it -import { Step, Steps } from "fumadocs-ui/components/steps"; -import { Callout } from "fumadocs-ui/components/callout"; +import { Tab, Tabs } from "fumadocs-ui/components/tabs";