refactor: remove config import from startup and update README for CLI usage

This commit is contained in:
Jakub Trávník 2026-01-02 10:35:18 +01:00
parent b25ed606b2
commit 04cbb58559
4 changed files with 27 additions and 76 deletions

View file

@ -1,6 +1,5 @@
import { eq } from "drizzle-orm";
import fs from "node:fs/promises";
import path from "node:path";
import slugify from "slugify";
import { db } from "../../db/db";
import {
@ -70,23 +69,6 @@ function interpolateEnvVars(value: unknown): unknown {
return value;
}
async function loadConfigFromFile(): Promise<unknown | null> {
try {
const configPath = process.env.ZEROBYTE_CONFIG_PATH || "zerobyte.config.json";
const configFullPath = path.resolve(process.cwd(), configPath);
try {
const raw = await fs.readFile(configFullPath, "utf-8");
return JSON.parse(raw);
} catch (error) {
if (isRecord(error) && error.code === "ENOENT") return null;
throw error;
}
} catch (error) {
logger.warn(`No config file loaded or error parsing config: ${toError(error).message}`);
return null;
}
}
function parseImportConfig(configRaw: unknown): ImportConfig {
const root = isRecord(configRaw) ? configRaw : {};
const config = isRecord(root.config) ? (root.config as Record<string, unknown>) : root;
@ -838,21 +820,3 @@ export async function applyConfigImport(configRaw: unknown, options: ImportOptio
logImportSummary(result);
return result;
}
/**
* Import configuration from a file (used by env var startup)
*/
export async function applyConfigImportFromFile(): Promise<void> {
const configRaw = await loadConfigFromFile();
if (configRaw === null) return; // No config file, nothing to do
logger.info("Starting config import from file...");
const config = parseImportConfig(configRaw);
try {
const result = await runImport(config);
logImportSummary(result);
} catch (e) {
logger.error(`Config import failed: ${toError(e).message}`);
}
}

View file

@ -10,7 +10,6 @@ import { VolumeHealthCheckJob } from "../../jobs/healthchecks";
import { RepositoryHealthCheckJob } from "../../jobs/repository-healthchecks";
import { BackupExecutionJob } from "../../jobs/backup-execution";
import { CleanupSessionsJob } from "../../jobs/cleanup-sessions";
import { applyConfigImportFromFile } from "./config-import";
import { repositoriesService } from "../repositories/repositories.service";
import { notificationsService } from "../notifications/notifications.service";
import { VolumeAutoRemountJob } from "~/server/jobs/auto-remount";
@ -45,13 +44,6 @@ export const startup = async () => {
await Scheduler.start();
await Scheduler.clear();
if (process.env.ZEROBYTE_CONFIG_IMPORT === "true") {
logger.info("Config import enabled (ZEROBYTE_CONFIG_IMPORT=true)");
await applyConfigImportFromFile();
} else {
logger.info("Config import skipped (set ZEROBYTE_CONFIG_IMPORT=true to enable)");
}
await restic.ensurePassfile().catch((err) => {
logger.error(`Error ensuring restic passfile exists: ${err.message}`);
});

View file

@ -1,6 +1,6 @@
# Config file import (Infrastructure as Code)
Zerobyte supports **config file import** on startup.
Zerobyte supports **config file import** via the CLI.
This lets you pre-configure volumes, repositories, backup schedules, notification destinations, and an initial user.
This example includes:
@ -13,7 +13,7 @@ This example includes:
- Docker + Docker Compose
This example includes `SYS_ADMIN` and `/dev/fuse` because its compatible with remote volume mounts (SMB/NFS/WebDAV).
This example includes `SYS_ADMIN` and `/dev/fuse` because it's compatible with remote volume mounts (SMB/NFS/WebDAV).
## Setup
@ -48,31 +48,19 @@ This is the recommended workflow for quick testing: if you don't have your own J
docker compose up -d
```
6. Access the UI at `http://localhost:4096`.
6. Run the config import:
```bash
docker compose exec zerobyte bun run cli import-config --config /app/zerobyte.config.json
```
7. Access the UI at `http://localhost:4096`.
## Notes
### Import methods
### CLI import command
Zerobyte supports two ways to import configuration:
#### Method 1: Environment variable (automatic on startup)
Set `ZEROBYTE_CONFIG_IMPORT=true` and the import runs automatically when the container starts:
```yaml
services:
zerobyte:
environment:
- ZEROBYTE_CONFIG_IMPORT=true
- ZEROBYTE_CONFIG_PATH=/app/zerobyte.config.json # optional, this is the default
```
This is ideal for automated deployments where you want `docker compose up` to fully configure the instance.
#### Method 2: CLI command (manual control)
Run the import explicitly using the CLI:
Import configuration using the CLI:
```bash
# Import from a mounted config file (starts a new temporary container)
@ -89,15 +77,23 @@ Get-Content zerobyte.config.json | docker compose exec -T zerobyte bun run cli i
# Validate config without importing (dry run)
docker compose run --rm zerobyte bun run cli import-config --config /app/zerobyte.config.json --dry-run
# Get JSON output for scripting
docker compose exec zerobyte bun run cli import-config --config /app/zerobyte.config.json --json
```
The `--stdin` option is useful when you don't want to mount the config file - just pipe it directly.
This is useful when you want to:
- See import output directly in your terminal
- Re-run import after fixing issues
- Test config files before applying them
- Import without modifying your docker-compose.yml
### CLI options
| Option | Description |
|--------|-------------|
| `--config <path>` | Path to the configuration file inside the container |
| `--stdin` | Read configuration from stdin |
| `--dry-run` | Validate the config without importing |
| `--json` | Output results in JSON format |
| `--log-level <level>` | Set log level (debug, info, warn, error) |
| `--overwrite-recovery-key` | Overwrite existing recovery key (only allowed if database is empty) |
### Secrets via env vars
@ -170,7 +166,7 @@ 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 Zerobytes stored config.
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:
@ -178,14 +174,14 @@ See the runnable example:
### Config file behavior (create-only)
The config file is applied on startup using a **create-only** approach:
The config file is applied using a **create-only** approach:
- **Volumes, notifications, schedules**: Skipped if a resource with the same name already exists
- **Repositories**: Skipped if any of these conditions are met:
- A repository pointing to the same location (path/bucket/endpoint) is already registered
- For local repos: the path is already a restic repository (set `isExistingRepository: true` to import it)
- A repository with the same name already exists
- Changes made via the UI are preserved across container restarts
- Changes made via the UI are preserved across imports
- 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".

View file

@ -13,7 +13,6 @@ services:
- .env
environment:
- TZ=${TZ:-UTC}
- ZEROBYTE_CONFIG_IMPORT=true
volumes:
- /etc/localtime:/etc/localtime:ro
- /var/lib/zerobyte:/var/lib/zerobyte