Automated setup and backup system for Docker-based homelab applications.
Warning
This project is being developed with the help of LLMs and agentic coding. Although I'm a professional software developer, I'm more experienced in developing websites and mobile apps.
Warning
Mithrandir has for now only been tested on a Raspberry Pi 5 with 4GB RAM and running Raspberry Pi OS in headless mode. It may not work on other hardware or operating systems.
Documentation website: https://joel-mercier.github.io/mithrandir/
git clone https://github.com/Joel-Mercier/mithrandir.git && cd mithrandir
sudo bash install.sh # Installs Bun + dependencies
mithrandir setup # No sudo needed — CLI elevates internally when requiredAll configuration lives in a single .env file at the project root.
Core settings:
BASE_DIR: Base directory where Docker app folders are located (e.g.,/opt/docker)PUID/PGID: User/group IDs for Docker containers (default:1000)TZ: Timezone (default:Etc/UTC)LOCAL_IP: Local IP address (auto-detected if not set)AUTO_YES: Set totrueto automatically accept all prompts (default:false)
Backup settings:
BACKUP_DIR: Local backup storage directory (default:/backups)LOCAL_RETENTION: Number of local backups to keep (default:5)REMOTE_RETENTION: Number of remote backups to keep (default:10)RCLONE_REMOTES: Comma-separated list of rclone remote names for cloud backups (default:gdrive)APPS: Apps to backup -"auto"to detect installed apps, or comma-separated listBACKUP_HOUR: Hour of the day when automatic backups run (0-23, default:2for 2:00 AM)BACKUP_PASSWORD: Optional encryption password — when set, backups are encrypted with AES-256-CBC
HTTPS settings (Caddy reverse proxy):
ENABLE_HTTPS: Set totruewhen HTTPS is installed (managed byinstall https)ACME_EMAIL: Email for Let's Encrypt certificate notifications
Firewall settings:
ENABLE_FIREWALL: Set totruewhen UFW firewall is installed (managed byinstall firewall)
SSO settings:
ENABLE_SSO: Set totrueto enable the Mithrandir UI as an OAuth/OIDC provider for homelab apps (default:false)
Per-app secrets:
DUCKDNS_SUBDOMAINS,DUCKDNS_TOKEN: Required for DuckDNSWG_SERVERURL: Required for WireGuardWG_PEERS: Number of WireGuard peers (default:1)IMMICH_DB_PASSWORD: Optional database password for Immich (default:postgres)SURE_SECRET_KEY_BASE: Required Rails secret for SureSURE_DB_PASSWORD: Optional database password for SureSURE_OPENAI_ACCESS_TOKEN: Optional OpenAI access token for Sure AI featuresSURE_OPENAI_URI_BASE: Optional OpenAI API base URI for SureSURE_OPENAI_MODEL: Optional OpenAI model name for SureAFFINE_DB_PASSWORD: Optional database password for AFFiNEAFFINE_DB_USERNAME: Optional database username for AFFiNE (default:affine)PAPERLESS_OCR_LANGUAGE: OCR language for Paperless-ngx (default:eng)PENPOT_SECRET_KEY: Required secret key for PenpotPENPOT_DB_PASSWORD: Optional database password for PenpotPENPOT_PUBLIC_URI: Optional public URI for Penpot (default:http://localhost:9001)ADVENTURELOG_SECRET_KEY: Required Django secret key for AdventureLogADVENTURELOG_DB_PASSWORD: Optional database password for AdventureLogADVENTURELOG_ADMIN_USERNAME: Optional admin username for AdventureLog (default:admin)ADVENTURELOG_ADMIN_PASSWORD: Optional admin password for AdventureLogADVENTURELOG_ADMIN_EMAIL: Optional admin email for AdventureLogPIHOLE_PASSWORD: Optional web interface password for Pi-holeYOURSPOTIFY_CLIENT_ID: Required Spotify Client ID for Your SpotifyYOURSPOTIFY_CLIENT_SECRET: Required Spotify Client Secret for Your SpotifyTANDOOR_SECRET_KEY: Required secret key for Tandoor (auto-generated during install)TANDOOR_DB_PASSWORD: Optional database password for Tandoor (default:tandoor)MEALIE_DB_PASSWORD: Optional database password for MealieYOUTARR_AUTH_USERNAME: Required admin username for YoutarrYOUTARR_AUTH_PASSWORD: Required admin password for YoutarrYOUTARR_DB_PASSWORD: Optional database password for YoutarrGATUS_DISCORD_WEBHOOK_URL: Optional Discord webhook URL for Gatus alerts
Mithrandir uses rclone to sync backups to one or more cloud storage providers. Supported providers include Google Drive, SFTP, S3, Dropbox, OneDrive, and iCloud Drive.
Add a remote interactively:
mithrandir backup remote addThis guided wizard walks you through selecting a provider, entering credentials, and testing connectivity. The remote is automatically added to RCLONE_REMOTES in .env.
Manage remotes:
mithrandir backup remote list # Show configured remotes with status
mithrandir backup remote remove # Remove a remoteAlternatively, run rclone config manually and ensure the remote name is listed in RCLONE_REMOTES in .env.
Google Drive auto-configuration:
Set all three values to auto-generate rclone.conf without running rclone config manually:
RCLONE_GDRIVE_APP_ID: Google Drive OAuth client ID (create your own)RCLONE_GDRIVE_APP_SECRET: Google Drive OAuth client secretRCLONE_GDRIVE_TOKEN: Google Drive OAuth token
Headless servers: OAuth-based providers (Google Drive, Dropbox, OneDrive) require a browser for initial authorization. Run
rclone authorize "<provider>"on a machine with a browser, then paste the resulting token duringmithrandir backup remote add. This is a one-time setup — automated backups work without a browser afterward.
Automatically generated and installed by setup to /etc/systemd/system/:
- Service:
homelab-backup.service- Runs backup as a oneshot service
- Path is automatically configured during setup
- Timer:
homelab-backup.timer- Runs daily at the configured hour (default: 2:00 AM) with 0-30 minute randomization
- Configurable via
BACKUP_HOURenv var ormithrandir backup config - Persistent: runs immediately if system was off during scheduled time
The CLI requires Bun. Run sudo bash install.sh first to install Bun, build the CLI, and install the mithrandir command on a bare Debian/Ubuntu server.
Privilege handling: Most commands work without sudo when your user is in the docker group. The CLI uses sudo internally only for system operations (apt, systemd, UFW). During initial setup, Docker is installed and your user is added to the docker group automatically — log out and back in for it to take effect. Until then, the CLI transparently falls back to sudo for Docker operations.
Setup wizard:
mithrandir setup [--yes]Interactive multi-step wizard: installs Docker and rclone, prompts for base directory, lets you pick services to install, auto configures the installed services, configures the systemd backup timer, and prints a summary with service URLs. --yes skips all prompts, selects all apps, and uses defaults from .env.
Backup:
mithrandir backupBacks up all configured apps. In a terminal it shows spinners and colored progress; from systemd (non-TTY) it writes timestamped plaintext to stdout and /var/log/homelab-backup.log.
List backups:
mithrandir backup list [local|remote]Lists existing backups with their contents. Without an argument, shows both local and remote backups.
Examples:
mithrandir backup list # List both local and remote backups
mithrandir backup list local # List only local backups
mithrandir backup list remote # List only remote backupsDelete backups:
mithrandir backup delete <local|remote> [YYYY-MM-DD] [--yes]local: Delete local backups from the archive directoryremote: Delete remote backups via rcloneYYYY-MM-DD: Optional date — deletes only that date's backup. Without a date, deletes all backups--yes: Skip confirmation prompt
Examples:
mithrandir backup delete local # Delete all local backups
mithrandir backup delete local 2025-06-01 # Delete a specific local backup
mithrandir backup delete remote --yes # Delete all remote backups (no prompt)
mithrandir backup delete remote 2025-06-01 # Delete a specific remote backupConfigure backup settings:
mithrandir backup configInteractive command to view and edit all backup settings: backup directory, retention counts, rclone remotes, apps to backup, backup hour, and encryption password. Saves changes to .env and updates the systemd timer automatically if the backup hour changes.
Verify backups:
mithrandir backup verify [YYYY-MM-DD] [--remote] [--extract]Checks archive integrity, validates expected files are present (docker-compose.yml, config dirs), and reports file sizes. Without a date, verifies the most recent backup.
--remote: Verify remote backups (downloads to temp dir, verifies, cleans up)--extract: Also perform a test extraction to a temp directory
Examples:
mithrandir backup verify # Verify most recent local backup
mithrandir backup verify 2025-06-01 # Verify a specific date
mithrandir backup verify --remote # Verify most recent remote backup
mithrandir backup verify --remote --extract # Verify remote with extract testRestore:
mithrandir restore <app|full> [date] [--yes]app: Name of app to restore (e.g.,jellyfin,radarr,sonarr)full: Restore all apps and secretsdate: Backup date inYYYY-MM-DDformat (default:latest)--yes: Skip confirmation prompts
Examples:
mithrandir restore jellyfin
mithrandir restore jellyfin 2025-01-01
mithrandir restore full
mithrandir restore full 2025-01-01 --yesDisaster recovery:
mithrandir recover [--yes]Full disaster recovery for a fresh system (new server, reinstalled OS). Automates the entire process: installs Docker and rclone, verifies rclone remotes are configured, sets up the base directory, discovers the latest remote backup (trying each configured remote in order), restores secrets and all app configs, regenerates docker-compose files, starts all containers, and installs the backup timer. Unlike restore (which assumes Docker and compose files already exist), recover bootstraps everything from scratch.
In interactive mode, prompts for confirmation at each step. In --yes mode, uses all defaults and fails if no rclone remote is configured.
Examples:
mithrandir recover # Interactive recovery
mithrandir recover --yes # Automated recovery with defaultsUninstall an app:
mithrandir uninstall <app>Stops and removes the container and companion apps. Prompts whether to also delete the app's data and configuration.
Full system removal:
mithrandir uninstallGuided 9-step removal with per-step prompts. Each destructive step (backups, rclone, app data, Docker, .env config) can be individually accepted or skipped, allowing you to keep Docker and apps running independently. Removes all Mithrandir systemd services (backup, UI, tusd), log files, CLI symlink, and cache. Also available from the web dashboard under Settings > General > Danger Zone.
Update containers:
mithrandir update [app] [--yes]Pulls the latest Docker images for installed apps and recreates containers that have newer images available. Optionally backs up apps before updating. Without an app name, updates all installed apps. --yes skips the backup confirmation prompt.
Examples:
mithrandir update # Update all installed apps
mithrandir update radarr # Update only Radarr
mithrandir update --yes # Update all, skip backup promptView logs:
mithrandir log <app> [--follow] [--tail N] [--since TIME]Streams Docker container logs to the terminal. Supports following output in real time, limiting the number of lines shown, and filtering by time.
Examples:
mithrandir log radarr # Show all logs
mithrandir log radarr --follow # Follow log output
mithrandir log radarr --tail 100 # Show last 100 lines
mithrandir log radarr --follow --tail 50 # Follow, starting from last 50 lines
mithrandir log jellyfin --since 1h # Logs from the last hourStart an app:
mithrandir start <app>Starts a stopped app container. The app must already be installed (docker-compose.yml exists).
Stop an app:
mithrandir stop <app>Stops a running app container.
Restart an app:
mithrandir restart <app>Stops and restarts a running app container.
Install an app:
mithrandir install <app>Installs a single app: pulls the Docker image, creates directories, generates docker-compose.yml, and starts the container. The app must not already be installed.
Install a stack:
mithrandir install <stack>Installs a predefined group of apps in one command. Already-installed apps are skipped. Companion apps are included automatically.
Available stacks: media, media-movies-tv, media-audio, media-pictures, media-games, security
Examples:
mithrandir install media-movies-tv # qBittorrent, Prowlarr, Radarr, Sonarr, Bazarr, Seerr, Jellyfin
mithrandir install security # Caddy, Pi-holeInstall Docker:
mithrandir install dockerInstalls Docker engine on the host. If Docker is already installed and running, reports the existing installation. Equivalent to the Docker installation step in the setup wizard.
Install backup system:
mithrandir install backupInstalls rclone (for remote backups to cloud storage) and sets up the systemd backup timer (daily at 2:00 AM). Skips components that are already installed. Equivalent to the rclone and backup timer steps in the setup wizard.
Install HTTPS:
mithrandir install httpsSets up HTTPS for all installed apps using Caddy as a reverse proxy with automatic Let's Encrypt certificates via DuckDNS DNS-01 challenge. Requires the DuckDNS app to be installed and running first.
What it does:
- Prompts for an ACME email (used by Let's Encrypt for certificate expiry warnings)
- Builds a custom Caddy Docker image with the DuckDNS DNS module
- Generates a Caddyfile with reverse proxy entries for all installed apps
- Starts the Caddy container on port 443
- If Pi-hole is installed, restarts it without port 443 (Caddy takes over)
After installation, apps are accessible at https://appname.yourdomain.duckdns.org. The Caddyfile is automatically regenerated whenever you install or uninstall an app.
DNS setup required: DuckDNS only creates an A record for the base domain (e.g. yourdomain.duckdns.org), not wildcard subdomains. You need to add a wildcard DNS entry on your router pointing *.yourdomain.duckdns.org to your server's LAN IP. How to do this depends on your router (OpenWrt, pfSense, UniFi all support custom DNS entries).
Reinstall an app:
mithrandir reinstall <app> [--yes]Completely reinstalls an app: stops the container, removes the Docker image, optionally deletes app data, then recreates directories, generates a fresh docker-compose.yml, pulls the image, and starts the container. --yes skips the data deletion prompt (deletes data without asking).
Examples:
mithrandir reinstall radarr # Reinstall, prompt before deleting data
mithrandir reinstall radarr --yes # Reinstall, delete data without promptingSelf-update:
mithrandir self-updatePulls the latest code from git, installs any new dependencies, and rebuilds the CLI. Since /usr/local/bin/mithrandir is a symlink to the built file, no reinstall is needed.
The CLI also checks for updates automatically once every 24 hours. When a newer version is available on the remote, a yellow notice is printed after the command output:
Update available (3 commits behind). Run `mithrandir self-update` to update.
The check runs concurrently with the command so it doesn't add latency. The last check timestamp is cached in ~/.cache/mithrandir/last-update-check. The check is skipped for self-update, version, and completions commands.
Version:
mithrandir versionPrints the CLI version and short git commit hash, e.g. mithrandir v1.0.0 (abc1234). Also available via mithrandir --version (version number only).
Config:
mithrandir configPretty-prints the current .env settings. Shows the file path so you know where to edit. Tokens and secrets are masked.
Shell completions:
mithrandir completions <bash|zsh|fish>Generates a shell completion script for the specified shell. Covers all subcommands, app names, flags, and backup/restore sub-arguments.
Examples:
eval "$(mithrandir completions bash)" # Bash (add to ~/.bashrc)
eval "$(mithrandir completions zsh)" # Zsh (add to ~/.zshrc)
mithrandir completions fish | source # Fish (add to config.fish)Documentation site:
mithrandir docs # Build and serve docs website
mithrandir docs stop # Stop docs website
bun run docs:dev # Local dev server (hot reload)Dependency graph:
mithrandir graphShows the inter-app dependency tree with color-coded installation status (green = installed, dim = not installed). Includes the media pipeline data flow, network/security dependencies, standalone apps, and recommended installation order for the Arr stack.
Status check:
mithrandir statusDisplays the status of all homelab components: installed apps, running containers, backup info, and disk usage.
Health check:
mithrandir healthValidates system health across five dimensions: Docker daemon, disk space (warn at 80%, fail at 95%), backup age (warn >2 days, fail >7 days), container restart loops (fail if RestartCount >5 or status "restarting"), and remote backup connectivity via rclone. Exit code 0 if all pass/warn, 1 if any fail — useful for monitoring and automation.
Capacity planning:
mithrandir capacityShows system hardware info (CPU, RAM, disk), per-app resource impact scores, and aggregate capacity verdicts. Helps you understand when your homelab is nearing its limits.
Doctor (diagnose setup issues):
mithrandir doctorChecks configuration correctness across three categories: System (.env file, Docker installation and daemon), Apps (stopped containers, missing config directories, missing required/optional secrets), and Backup (backup directory, systemd service and timer, rclone installation and remote configuration). Each failing or warning check includes an actionable hint with the command to fix it. Backup checks are skipped if no apps are installed. Exit code 1 if any check fails.
| App | Port | Description |
|---|---|---|
| Actual Budget | 5006 | Privacy-focused personal finance and budgeting app |
| Audiobookshelf | 13378 | Self-hosted audiobook and podcast server |
| AdventureLog | 8015 | Travel planning and adventure journal |
| AFFiNE | 3010 | Privacy-focused knowledge base and workspace |
| Bazarr | 6767 | Subtitle manager for Sonarr and Radarr |
| Byparr | 8192 | FlareSolverr-compatible proxy to bypass Cloudflare (auto-installed with Prowlarr) |
| Caddy | — | HTTPS reverse proxy with automatic certificates (hidden, installed via install https) |
| CookCLI | 9080 | Recipe manager using the Cooklang markup language |
| Mealie | 9925 | Self-hosted recipe manager and meal planner |
| Memos | 5230 | Lightweight self-hosted memo hub and knowledge management |
| DuckDNS | — | Free dynamic DNS service |
| Excalidraw | 5000 | Virtual whiteboard for sketching |
| Gatus | 3001 | Automated service health monitoring |
| Glance | 8082 | Self-hosted dashboard with various widgets |
| Home Assistant | 8123 | Open-source home automation platform |
| Homarr | 7575 | Customizable dashboard for your server |
| Immich | 2283 | Self-hosted photo and video management |
| Jellyfin | 8096 | Free media streaming server |
| Lidarr | 8686 | Music collection manager |
| n8n | 5678 | Workflow automation platform |
| Navidrome | 4533 | Modern music server and streamer |
| Omni Tools | 8079 | Collection of useful productivity tools |
| Open WebUI | 3000 | Self-hosted AI chat interface |
| Paperless-ngx | 8000 | Document management system with OCR |
| Penpot | 9001 | Open-source design and prototyping platform |
| Pi-hole | 80 | Network-wide ad blocker and DNS server |
| Profilarr | 6868 | Quality profile manager for Radarr and Sonarr |
| Prowlarr | 9696 | Indexer manager for the *Arr stack (also installs Byparr) |
| qBittorrent | 8080 | BitTorrent client with web UI |
| Radarr | 7878 | Movie collection manager |
| RetroAssembly | 8001 | Personal retro game collection cabinet in your browser |
| Seerr | 5055 | Media request manager for Jellyfin |
| Sonarr | 8989 | TV series collection manager |
| Stirling PDF | 8084 | All-in-one PDF manipulation tool |
| Sure | 3005 | Privacy-focused personal finance tracker |
| Tandoor | 9010 | Recipe manager and meal planner with shopping lists |
| TRIP | 8085 | Travel planning and trip journal |
| Vaultwarden | 8222 | Lightweight Bitwarden-compatible password manager (requires HTTPS) |
| WireGuard | 51820/udp | Fast, modern VPN tunnel |
| Youtarr | 3087 | YouTube video downloader and manager |
| Your Spotify | 3456 | Spotify listening statistics and history tracker |
Mithrandir includes a web-based dashboard for managing your homelab from a browser. It provides the same capabilities as the CLI in a visual interface.
Tech stack: TanStack Start (SSR) + React 19 + Vite, styled with Tailwind CSS v4 and shadcn/ui components. Authentication via Better-Auth with email/password, optional two-factor (TOTP), and optional OIDC SSO (Authentik, Keycloak, Authelia, etc.). Data is stored in a local SQLite database via Drizzle ORM. Internationalized with Paraglide (English and French).
Features:
- Dashboard — Overview of system status, installed apps, backup status, resource usage, and configuration at a glance. Includes a doctor dialog for diagnosing issues.
- App management — Browse all available apps with search and category filtering, view app details, install/uninstall/start/stop/restart apps, and stream container logs in real time.
- Dependency graph — Visual representation of inter-app dependencies with installation status.
- Capacity planning — System resource overview with per-app performance and storage scores, rendered with score rings and storage meters.
- Backup & restore — View local and remote backups, trigger backups, and restore apps or full system from the UI.
- Setup wizard — Step-by-step guided setup with the same workflow as the CLI wizard.
- Media library — Browse media files on the server with a file tree viewer.
- File upload — Upload files to the server via resumable uploads (tus protocol with Uppy).
- Settings — Configure general, backup, and network settings. View system information. Includes a Danger Zone for complete system removal with per-step prompts.
- User profile — Manage account, active sessions, and two-factor authentication.
- Self-update — Update Mithrandir from git directly in the browser with a step-by-step progress view.
- Dark mode — Light, dark, and auto themes with a toggle in the header.
Deployment:
The UI runs in production as two systemd services:
mithrandir-ui.service— Serves the TanStack Start SSR app on port 4180. Runs database migrations on startup, loads env vars from both.envandui/.env.local.mithrandir-tusd.service— Runs tusd, a resumable upload server, on port 1080. Handles chunked file uploads and forwards lifecycle hooks (pre-create, post-finish) to the UI for authentication and processing.
Both services are installed automatically by mithrandir ui and managed via systemd (enable, start, stop, restart). When Caddy HTTPS is enabled, the Caddyfile is regenerated to reverse-proxy both services.
Builds use a blue-green deployment strategy: new builds are placed in an inactive slot under ui/.deployments/, then an atomic symlink swap points current to the new build. This allows zero-downtime updates.
Self-update from the web UI (or mithrandir self-update) logs each step to /var/log/mithrandir-ui-update.log with timestamps. The log covers git pull, dependency install, CLI build, UI build, deployment, and service restart — useful for diagnosing update failures.
Running the UI:
mithrandir ui # Build, deploy, and start both services (production)
mithrandir ui stop # Stop both services
bun run ui:dev # Development server on port 3000 (hot reload)
bun run ui:build # Build for production
bun run ui:preview # Preview the production buildThe UI requires a .env.local file in the ui/ directory with BETTER_AUTH_URL and BETTER_AUTH_SECRET (a 32-character secret, generate with openssl rand -base64 32) for authentication. When started via mithrandir ui, this file is auto-generated if missing.
OIDC SSO (optional): To enable single sign-on via an external identity provider, add OIDC_CLIENT_ID, OIDC_CLIENT_SECRET, and OIDC_ISSUER_URL to ui/.env.local. Supports any OIDC-compliant provider (Authentik, Keycloak, Authelia, etc.). Set the redirect URI in your IdP to {BETTER_AUTH_URL}/api/auth/oauth2/callback/oidc.
The project is a Bun workspaces monorepo. Each subproject (cli/, docs/, ui/) has its own package.json with workspace-scoped dependencies, while the root package.json provides global proxy scripts and a single bun.lock. Running bun install at the root installs everything.
git clone https://github.com/Joel-Mercier/mithrandir.git && cd mithrandir
bun installCreating a release:
scripts/release.sh 1.1.0 # Bumps version, generates changelog, commits, and tags
git push && git push --tags # Push the releaseThe release script bumps the version in cli/package.json and docs/.vitepress/config.ts, regenerates docs/changelog.md from git tags, creates a commit, and tags it. The changelog groups commits by tag, with unreleased commits shown at the top.
You can also regenerate the changelog manually at any time:
scripts/generate-changelog.shAvailable scripts:
| Command | Description |
|---|---|
bun run cli:start |
Run the CLI in dev mode (unbundled) |
bun run cli:build |
Bundle into cli/dist/mithrandir.js |
bun run cli:test |
Run CLI unit and snapshot tests |
bun run cli:typecheck |
TypeScript type checking for CLI |
bun run ui:dev |
Local Vite dev server with hot reload |
bun run ui:build |
Build the UI for production |
bun run ui:preview |
Preview the built UI |
bun run ui:test |
Run UI unit and snapshot tests |
bun run ui:format |
Format all UI files |
bun run ui:lint |
Lint all UI files |
bun run ui:check |
Biome checking for UI |
bun run ui:typecheck |
TypeScript type checking for UI |
bun run build |
Build all workspaces |
bun run test |
Run tests across all workspaces |
bun run typecheck |
TypeScript type checking for all workspaces |
bun run docs:dev |
Local VitePress dev server with hot reload |
bun run docs:build |
Build the documentation site for production |
bun run docs:preview |
Preview the built documentation site |
bun run release <version> |
Create a new release |
Tests use Bun's built-in test runner. Test files are in cli/src/__tests__/:
- App registry (
apps.test.ts) — validates app lookups, container names, config paths, conflict filtering, stacks, and registry integrity - Config parsing (
config.test.ts) — tests.envloading (KEY=VALUE, quotes,exportprefix, comments) and backup config defaults - Compose generation (
compose.test.ts) — snapshot tests for docker-compose.yml output across all app types (standard, host-networked, secrets, healthchecks, capabilities, multi-config, port remapping, rawCompose) - Caddy generation (
caddy.test.ts) — tests domain derivation, Caddyfile generation, 404 page, and Dockerfile output - Backup utilities (
backup-utils.test.ts) — archive suffix stripping, backup archive detection, and archive filename generation - Crypto (
crypto.test.ts) — encrypted backup file detection - Systemd (
systemd.test.ts) — service and timer unit generation with snapshot tests - Swap (
swap.test.ts) — swap size formatting (GB/MB thresholds, edge cases) - Logger (
logger.test.ts) — log message formatting, timestamp pattern validation, and log path constants
Snapshot files are stored in cli/src/__tests__/__snapshots__/ and committed to git. When compose or caddy generation logic changes, update snapshots with:
bun test --update-snapshotsVM-based end-to-end tests live in cli/integration-tests/ using nix-vm-test. Debian 13 VMs are spun up via QEMU to test critical CLI paths: install flow, Docker setup, app lifecycle, backup/restore, diagnostics, and updates. Requires a Linux host with KVM (runs in CI on GitHub Actions with hardware-accelerated KVM).
See cli/integration-tests/README.md for details on running locally and writing new tests.
A GitHub Actions workflow runs on every push and pull request to main. It runs bun run typecheck, bun run build, and bun run test across all workspaces. A parallel matrix of integration tests spins up Debian VMs to verify the install flow, Docker setup, app lifecycle, backup/restore, diagnostics, and update commands.