diff --git a/app/server/modules/lifecycle/config-import.ts b/app/server/modules/lifecycle/config-import.ts index 728dc070..76822b17 100644 --- a/app/server/modules/lifecycle/config-import.ts +++ b/app/server/modules/lifecycle/config-import.ts @@ -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 { - 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) : 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 { - 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}`); - } -} diff --git a/app/server/modules/lifecycle/startup.ts b/app/server/modules/lifecycle/startup.ts index 52d3f7cc..aaf9475b 100644 --- a/app/server/modules/lifecycle/startup.ts +++ b/app/server/modules/lifecycle/startup.ts @@ -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}`); }); diff --git a/examples/config-file-import/README.md b/examples/config-file-import/README.md index 971b0a21..978b7a82 100644 --- a/examples/config-file-import/README.md +++ b/examples/config-file-import/README.md @@ -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 it’s 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 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 ` | 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 Zerobyte’s 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". diff --git a/examples/config-file-import/docker-compose.yml b/examples/config-file-import/docker-compose.yml index 78792779..fadfaea2 100644 --- a/examples/config-file-import/docker-compose.yml +++ b/examples/config-file-import/docker-compose.yml @@ -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