388 lines
8.1 KiB
Markdown
388 lines
8.1 KiB
Markdown
# Config file import (Infrastructure as Code)
|
||
|
||
Zerobyte supports **config file import** on startup.
|
||
This lets you pre-configure volumes, repositories, backup schedules, notification destinations, and an initial user.
|
||
|
||
This example includes:
|
||
|
||
- a runnable `docker-compose.yml`
|
||
- a comprehensive `zerobyte.config.example.json` template (trim it down to what you actually use)
|
||
- `.env.example` showing how to inject secrets via environment variables
|
||
|
||
## Prerequisites
|
||
|
||
- Docker + Docker Compose
|
||
|
||
This example includes `SYS_ADMIN` and `/dev/fuse` because it’s compatible with remote volume mounts (SMB/NFS/WebDAV).
|
||
|
||
## Setup
|
||
|
||
1. Copy the env file:
|
||
|
||
```bash
|
||
cp .env.example .env
|
||
```
|
||
|
||
2. Create a local directory to mount as a sample volume:
|
||
|
||
```bash
|
||
mkdir -p mydata
|
||
```
|
||
|
||
3. Create a working config file (copy the example template):
|
||
|
||
```bash
|
||
cp zerobyte.config.example.json zerobyte.config.json
|
||
```
|
||
|
||
This is the recommended workflow for quick testing: if you don't have your own JSON config yet, start from the template.
|
||
|
||
4. Review/edit `zerobyte.config.json`.
|
||
|
||
The example template is intentionally "kitchen-sink" (lots of volume/repository/notification types) so you can copy what you need.
|
||
Delete the entries you don't plan to use, and keep only the ones you have credentials/mounts for.
|
||
|
||
5. Start Zerobyte:
|
||
|
||
```bash
|
||
docker compose up -d
|
||
```
|
||
|
||
6. Access the UI at `http://localhost:4096`.
|
||
|
||
## Notes
|
||
|
||
### Enabling import
|
||
|
||
Config import is opt-in and only runs when:
|
||
|
||
- `ZEROBYTE_CONFIG_IMPORT=true`
|
||
|
||
The config path defaults to `/app/zerobyte.config.json`, but you can override it via:
|
||
|
||
- `ZEROBYTE_CONFIG_PATH=/app/your-config.json`
|
||
|
||
### Secrets via env vars
|
||
|
||
Zerobyte supports **two different mechanisms** that are easy to confuse:
|
||
|
||
1. **Config import interpolation** (this example)
|
||
2. **Secret placeholders** (`env://...` and `file://...`)
|
||
|
||
#### 1) Config import interpolation: `${VAR_NAME}`
|
||
|
||
During config import, any string value in the JSON can reference an environment variable using `${VAR_NAME}`.
|
||
|
||
Example:
|
||
|
||
```json
|
||
{
|
||
"recoveryKey": "${RECOVERY_KEY}",
|
||
"repositories": [
|
||
{
|
||
"name": "s3-repo",
|
||
"config": {
|
||
"backend": "s3",
|
||
"accessKeyId": "${ACCESS_KEY_ID}",
|
||
"secretAccessKey": "${SECRET_ACCESS_KEY}"
|
||
}
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
Important properties of `${...}` interpolation:
|
||
|
||
- It runs **only during import**.
|
||
- Values are **resolved before** they are written to the database (meaning the actual secret ends up in the DB for fields that are stored as secrets).
|
||
- Because it reads `process.env`, Docker Compose must inject those variables into the container.
|
||
|
||
This example uses:
|
||
|
||
- `env_file: .env`
|
||
|
||
So to make `${VAR_NAME}` work, put the variables in `.env` (or otherwise provide them in the container environment).
|
||
|
||
#### 2) Secret placeholders: `env://...` and `file://...`
|
||
|
||
Separately from config import, Zerobyte supports **secret placeholders** for *some sensitive fields*.
|
||
These placeholders are stored **as-is** in the database (the raw secret is not stored) and resolved at runtime.
|
||
|
||
Supported formats:
|
||
|
||
- `env://VAR_NAME` → reads `process.env.VAR_NAME` at runtime
|
||
- `file://secret_name` → reads `/run/secrets/secret_name` (Docker secrets)
|
||
|
||
This is useful when you want to keep secrets out of the database and rotate them without editing Zerobyte’s stored config.
|
||
|
||
See the runnable example:
|
||
|
||
- [examples/secrets-placeholders/README.md](../secrets-placeholders/README.md)
|
||
|
||
### Config file behavior (create-only)
|
||
|
||
The config file is applied on startup using a **create-only** approach:
|
||
|
||
- Resources defined in the config are only created if they don't already exist in the database
|
||
- Existing resources with the same name are **not overwritten** (a warning is logged and the config entry is skipped)
|
||
- Changes made via the UI are preserved across container restarts
|
||
- To update a resource from config, either modify it via the UI or delete it first
|
||
|
||
This makes the config file better suited as "initial setup" than as a "desired state sync".
|
||
|
||
---
|
||
|
||
## Config structure reference
|
||
|
||
This example is intended to be the primary, copy/paste-friendly reference for config import.
|
||
|
||
### `zerobyte.config.json` structure
|
||
|
||
```json
|
||
{
|
||
"recoveryKey": "${RECOVERY_KEY}",
|
||
"volumes": [
|
||
"..."
|
||
],
|
||
"repositories": [
|
||
"..."
|
||
],
|
||
"backupSchedules": [
|
||
"..."
|
||
],
|
||
"notificationDestinations": [
|
||
"..."
|
||
],
|
||
"users": [
|
||
"..."
|
||
]
|
||
}
|
||
```
|
||
|
||
### Volume types
|
||
|
||
#### Local directory
|
||
|
||
```json
|
||
{
|
||
"name": "local-volume",
|
||
"config": {
|
||
"backend": "directory",
|
||
"path": "/mydata",
|
||
"readOnly": true
|
||
}
|
||
}
|
||
```
|
||
|
||
#### NFS
|
||
|
||
```json
|
||
{
|
||
"name": "nfs-volume",
|
||
"config": {
|
||
"backend": "nfs",
|
||
"server": "nfs.example.com",
|
||
"exportPath": "/data",
|
||
"port": 2049,
|
||
"version": "4",
|
||
"readOnly": false
|
||
}
|
||
}
|
||
```
|
||
|
||
#### SMB
|
||
|
||
```json
|
||
{
|
||
"name": "smb-volume",
|
||
"config": {
|
||
"backend": "smb",
|
||
"server": "smb.example.com",
|
||
"share": "shared",
|
||
"username": "user",
|
||
"password": "${SMB_PASSWORD}",
|
||
"vers": "3.0",
|
||
"domain": "WORKGROUP",
|
||
"port": 445,
|
||
"readOnly": false
|
||
}
|
||
}
|
||
```
|
||
|
||
#### WebDAV
|
||
|
||
```json
|
||
{
|
||
"name": "webdav-volume",
|
||
"config": {
|
||
"backend": "webdav",
|
||
"server": "webdav.example.com",
|
||
"path": "/remote.php/webdav",
|
||
"username": "user",
|
||
"password": "${WEBDAV_PASSWORD}",
|
||
"port": 80,
|
||
"readOnly": false,
|
||
"ssl": true
|
||
}
|
||
}
|
||
```
|
||
|
||
### Repository types
|
||
|
||
#### Local
|
||
|
||
```json
|
||
{
|
||
"name": "local-repo",
|
||
"config": {
|
||
"backend": "local",
|
||
"path": "/var/lib/zerobyte/repositories"
|
||
},
|
||
"compressionMode": "auto"
|
||
}
|
||
```
|
||
|
||
Note for importing existing local repositories (migration):
|
||
|
||
- include `config.name` and set `config.isExistingRepository: true`
|
||
- the actual restic repo is stored at `{path}/{name}`
|
||
|
||
```json
|
||
{
|
||
"name": "my-local-repo",
|
||
"config": {
|
||
"backend": "local",
|
||
"path": "/var/lib/zerobyte/repositories",
|
||
"name": "abc123",
|
||
"isExistingRepository": true
|
||
}
|
||
}
|
||
```
|
||
|
||
#### S3-compatible
|
||
|
||
```json
|
||
{
|
||
"name": "backup-repo",
|
||
"config": {
|
||
"backend": "s3",
|
||
"bucket": "mybucket",
|
||
"accessKeyId": "${ACCESS_KEY_ID}",
|
||
"secretAccessKey": "${SECRET_ACCESS_KEY}"
|
||
},
|
||
"compressionMode": "auto"
|
||
}
|
||
```
|
||
|
||
#### Google Cloud Storage
|
||
|
||
```json
|
||
{
|
||
"name": "gcs-repo",
|
||
"config": {
|
||
"backend": "gcs",
|
||
"bucket": "mybucket",
|
||
"projectId": "my-gcp-project",
|
||
"credentialsJson": "${GCS_CREDENTIALS}"
|
||
}
|
||
}
|
||
```
|
||
|
||
#### Azure Blob Storage
|
||
|
||
```json
|
||
{
|
||
"name": "azure-repo",
|
||
"config": {
|
||
"backend": "azure",
|
||
"container": "mycontainer",
|
||
"accountName": "myaccount",
|
||
"accountKey": "${AZURE_KEY}"
|
||
}
|
||
}
|
||
```
|
||
|
||
### Backup schedules
|
||
|
||
```json
|
||
{
|
||
"name": "local-volume-local-repo",
|
||
"volume": "local-volume",
|
||
"repository": "local-repo",
|
||
"cronExpression": "0 2 * * *",
|
||
"retentionPolicy": { "keepLast": 7, "keepDaily": 7 },
|
||
"includePatterns": ["important-folder"],
|
||
"excludePatterns": ["*.tmp", "*.log"],
|
||
"enabled": true,
|
||
"notifications": ["slack-alerts", "email-admin"]
|
||
}
|
||
```
|
||
|
||
`notifications` can also be an array of objects:
|
||
|
||
```json
|
||
[
|
||
{
|
||
"name": "slack-alerts",
|
||
"notifyOnStart": false,
|
||
"notifyOnSuccess": true,
|
||
"notifyOnWarning": true,
|
||
"notifyOnFailure": true
|
||
}
|
||
]
|
||
```
|
||
|
||
### User setup (automated)
|
||
|
||
Zerobyte currently supports a **single user**.
|
||
If multiple entries are provided in `users[]`, only the first one will be applied.
|
||
|
||
New instance:
|
||
|
||
```json
|
||
{
|
||
"recoveryKey": "${RECOVERY_KEY}",
|
||
"users": [
|
||
{
|
||
"username": "my-user",
|
||
"password": "${ADMIN_PASSWORD}"
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
Migration:
|
||
|
||
```json
|
||
{
|
||
"recoveryKey": "${RECOVERY_KEY}",
|
||
"users": [
|
||
{
|
||
"username": "my-user",
|
||
"passwordHash": "$argon2id$v=19$m=19456,t=2,p=1$..."
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
Use either `password` OR `passwordHash`, not both.
|
||
|
||
### Recovery key
|
||
|
||
The recovery key is a 64-character hex string that serves two critical purposes:
|
||
|
||
1. Restic repository password (encrypts your backup data)
|
||
2. Database encryption key (encrypts credentials stored in Zerobyte)
|
||
|
||
Generating a recovery key ahead of time:
|
||
|
||
```bash
|
||
# Using OpenSSL (Linux/macOS)
|
||
openssl rand -hex 32
|
||
|
||
# Using Python
|
||
python3 -c "import secrets; print(secrets.token_hex(32))"
|
||
|
||
# Using Docker (prints the key, container is removed)
|
||
docker run --rm python:3.12-alpine sh -lc 'echo "Key is on the next line:"; python -c "import secrets; print(secrets.token_hex(32))"'
|
||
```
|