Joulenap

Your Proxmox backup servers sleep. Joulenap wakes them, runs the backups, and tucks them back in.

CI License: AGPL v3 Docker image

Joulenap is a small self-hosted **web UI + scheduler** that runs automated Proxmox VE backups to **Proxmox Backup Servers (PBS) that stay powered off** most of the time. At the scheduled hour it wakes the backup server over the network (Wake-on-LAN), runs the job, applies retention and garbage collection, powers it back down, and notifies you โ€” so you get deduplicated backups **without keeping a second machine running 24/7**. You describe your setup as **routes**: which Proxmox hosts back up to which backup server, on what schedule, with what retention. A route can fan several PVE hosts into one PBS, or copy one PBS to another for a genuinely off-site second copy โ€” and Joulenap wakes and sleeps every box each route touches, in the right order. The name says it: a *joule* saved, while your backup server takes a *nap*. ๐Ÿ’ค --- ## Why A dedicated PBS box is the right way to keep backups on separate hardware (3-2-1 rule), but leaving it on 24/7 wastes power for a job that runs a few minutes a night. Proxmox's built-in scheduled backups assume the target is always reachable, so they can't drive a "wake โ†’ backup โ†’ sleep" cycle. Neither can they drive one for a *second* backup server that only comes up once a week to take an off-site copy. Joulenap fills that gap with a friendly UI: draw the routes, pick the times, pick which guests go where, and forget it. ## How it works
Joulenap **owns the schedule** itself (internal scheduler), so nothing on the Proxmox host needs to be modified. It talks to every PVE and PBS through their **APIs** (scoped tokens, one per device) and uses a single **SSH** command only for the power-off, which has no API. ## Features - ๐Ÿ”€ **Routes**: any number of Proxmox hosts and backup servers, wired together explicitly โ€” fan several PVEs into one PBS, or copy one PBS to another (**off-site sync**, pull or push) - โฐ A schedule per route, with its own retention, guest selection and options โ€” plus a global pause switch - ๐Ÿ”Œ Wake-on-LAN of every backup server a route touches, with readiness wait, retries and timeout; boxes you keep always on are supported too - ๐Ÿงต One run at a time, the rest queued โ€” and a box stays awake between two runs that both need it instead of being woken twice - ๐Ÿ—‚๏ธ Per-source guest selection: back up **all** guests of a host (new ones included automatically) or an explicit **include** list - โ™ป๏ธ Retention (last/daily/weekly/monthly/yearly), plus optional Garbage Collection and verification after a route runs - ๐Ÿ‘€ **External schedules**: a route kind that starts nothing of its own โ€” PVE/PBS run their own jobs, Joulenap just wakes the box, watches the tasks and powers it off when they go quiet - ๐Ÿ”” Notifications: Apprise, Telegram, ntfy, Discord, email โ€” on success and/or failure, per route - ๐Ÿ“œ Live log viewer, run history with a per-step timeline, live PVE/PBS task output, and manual runs โ€” per route, or an ad-hoc GC/verify on one box โ€” stoppable mid-run - โš™๏ธ Advanced settings tab with a built-in `config.yaml` editor, plus an opt-in update check - ๐Ÿ“Š Integrations: backup status for Homepage, Homarr, Dashy or Glance, plus a Prometheus `/metrics` endpoint for Grafana (alert when a guest stops being backed up) โ€” see [`docs/INTEGRATIONS.md`](docs/INTEGRATIONS.md) - ๐ŸŒ Multi-language UI - ๐Ÿ”’ Login-protected; secrets kept out of the repo ## Status **v1.0.0.** Built around routes over any number of PVE and PBS devices: backup, PBSโ†’PBS sync, external-schedule watching and verification, driven by a run queue and a per-server power lease so a box is woken once and slept once no matter how many routes need it. Packaged as a Docker image, with transport hardening (per-device PBS TLS pinning + SSH host-key verification) and auth hardening (login rate-limit, session hardening). Includes guided wizards for adding a PVE or a PBS, run history with a per-step timeline and live task output, the ability to stop a run mid-flight, [integrations](docs/INTEGRATIONS.md) for dashboards (Homepage/Homarr/Dashy/Glance) and Prometheus, persistent datastore usage shown even while a server is powered off, a per-channel notification test report, and a responsive UI that works on a phone. See [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) for the design and API. **Known limitation.** Joulenap reads the **root namespace** of a PBS datastore: if your Proxmox storage entry writes into a namespace, backups, retention and garbage collection all work, but *Last backup per guest* reads "never" for those guests. **Upgrading from 0.9?** Your `config.yaml` is converted automatically on the first start โ€” see [Upgrading](#upgrading-from-09) below before you pull. ## Quick start (Docker) One command โ€” no files to download, no config to edit first. The container creates its own config on first run and you fill it in through the web UI: ```bash mkdir -p /opt/joulenap/data docker run -d --name joulenap \ --restart unless-stopped \ --network host \ -e TZ=Etc/UTC \ -v /opt/joulenap/data:/app/data \ catubba/joulenap:latest # then open http://:8080 ``` `--network host` lets Joulenap send the Wake-on-LAN magic packet on your LAN broadcast; the single `data` directory persists config, history, logs and the SSH key across updates. You pick your **timezone on the first-run screen** (pre-detected from your browser), so the `TZ` above is just a neutral default. Prefer Compose? See [`docker-compose.example.yml`](docker-compose.example.yml). ๐Ÿ“– **Full guide:** [`docs/INSTALL.md`](docs/INSTALL.md) walks a **Proxmox LXC** install from scratch (create the container โ†’ install Docker โ†’ run Joulenap), plus Docker Compose and a native no-Docker install, timezone, and first-run setup. Every config field is documented in [`config.example.yaml`](config.example.yaml). ## Configuration All settings live in `config.yaml` (see [`config.example.yaml`](config.example.yaml) for every field, grouped and commented). You normally never touch it by hand โ€” the container creates it on first run inside the mounted `data/` directory, and the **wizards** under **Settings โ†’ Devices โ†’ + Add** fill it in: one flow adds a Proxmox host and discovers the backup servers it already knows about, the other adds a backup server and sets up its wake-up and power-off. Routes are then drawn from the homepage. Secrets (API tokens, SSH key, bot token) stay in that `config.yaml`; the repo's copy is **git-ignored** so it's never committed. ## Upgrading from 0.9 Pull the new image and start it โ€” nothing else. On the first start Joulenap converts your `config.yaml` from the old single-PVE/single-PBS layout into devices and routes: your backup job becomes a route named **Backup**, a scheduled verification becomes one named **Verify**, and your schedule, guest selection and retention come across with them. - **A copy of the old file is kept** as `config.yaml.pre-overhaul.bak` next to it, before anything is rewritten. If the conversion doesn't validate, your file is left untouched โ€” but Joulenap then starts with **no devices and no routes**, so nothing is scheduled and no backup runs until you fix it. That is what the banner on the dashboard is telling you: it is not a cosmetic warning, it is the reason the page looks empty. The `.bak` is your rollback โ€” see below for how to use it. - **One conversion is lossy, and it widens rather than narrows.** The old "back up all guests **except** these" mode no longer exists, so such a route becomes "all guests" โ€” it will back up *more* than before, never less. Narrow it down from the route editor if that isn't what you want. It's the one thing worth checking after the upgrade. - **Breaking for anything outside the UI**: `GET /api/dashboard` and `/metrics` changed shape, because there is no longer a single "next run" or "the datastore". Dashboard widgets and Grafana alerts built on 0.9 need updating โ€” the field-by-field mapping is at the top of [`docs/INTEGRATIONS.md`](docs/INTEGRATIONS.md).
Going back to 0.9 โ€” the config rolls back on its own, the database needs one step 1.0 widened the two *cache* tables (`guest_backups`, `datastore_stats`) with columns 0.9 never writes, so 0.9 left on a 1.0 database logs a cache warning after every backup and, worse, returns a **500 from the dashboard whenever a backup server is awake**. Dropping the two caches fixes it and keeps your run history โ€” both versions rebuild them from the backup server: ```bash docker rm -f joulenap cd /opt/joulenap/data cp config.yaml.pre-overhaul.bak config.yaml docker run --rm -v /opt/joulenap/data:/app/data catubba/joulenap:0.9.0 python -c \ "import sqlite3; d = sqlite3.connect('/app/data/joulenap.db'); \ d.execute('DROP TABLE IF EXISTS guest_backups'); \ d.execute('DROP TABLE IF EXISTS datastore_stats'); d.commit()" # then start 0.9 with your usual docker run, pinned to catubba/joulenap:0.9.0 ``` If you would rather not run that, `mv joulenap.db joulenap.db.1.0` instead โ€” 0.9 builds a fresh one and you lose only the run history. Your backups live on the backup server and none of this touches them, and forgetting the config step is safe: 0.9 refuses to start on a 1.0 config rather than coming up empty. **0.9 gets no fixes, including security fixes** ([`SECURITY.md`](SECURITY.md)). This is a way to buy an evening, not a place to stay โ€” please open an issue for whatever sent you back.
## Security Joulenap can trigger backups and power machines on/off, so treat it as privileged: - Use **scoped API tokens** for each PVE and each PBS, not root passwords โ€” the exact privileges each one needs are listed in [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md#permissions-cheat-sheet). - The SSH key to a backup server should be dedicated and, ideally, restricted to the power-off command โ€” the wizard offers exactly such a line. - **A PBS API connection is TLS-pinned** to that device's stored certificate fingerprint, so a swapped/MITM cert is rejected; a legitimately renewed cert is accepted after you re-run that device's connect step in the wizard. The wizard always captures the fingerprint โ€” but a device left without one is **not pinned and not validated**, so keep it set. - **A PBS SSH host key is verified** against `data/known_hosts`. The wizard's automatic key-install path confirms it with you first; a server added straight from Settings โ†’ Devices, or one where you installed the key yourself, is trusted on first use and recorded, with a warning in the log. Details in [`docs/CONFIG-WIZARD.md`](docs/CONFIG-WIZARD.md#security). - Keep the UI on your LAN/VPN and behind its login. Don't expose it to the internet. - `config.yaml` holds secrets โ€” keep its file permissions tight and out of version control. - **Login lockout**: after 5 failed login attempts from an IP address, that IP is locked out for 5 minutes (protects against online brute-force attacks). - **Password floor**: admin passwords must be at least 8 characters. - **Session cookie** (`app.session` in config): set `https_only: true` when serving Joulenap over HTTPS or behind a TLS-terminating proxy; `max_age_days` controls session lifetime (default 14 days). Changing the admin password immediately signs out every *other* session; the one you changed it from stays signed in. - **First-run setup**: complete the initial account setup promptly โ€” the setup endpoint remains open until an account is created (and is rate-limited for security). ## Roadmap - [โœ…] v0.1: scheduler + WoL + vzdump + retention + notifications + web UI - [โœ…] Garbage Collection after each backup, and scheduled verify jobs - [โœ…] Per-guest last-backup status from PBS - [โœ…] v1.0: multiple PVE and PBS devices, routes, and PBSโ†’PBS off-site sync - [ ] RTC-wake option (BIOS alarm) as an alternative to WoL - [ ] Per-route notification routing (which channel hears about which route) ## License Licensed under the **GNU Affero General Public License v3.0** (AGPL-3.0). See [`LICENSE`](LICENSE). ## Disclaimer Joulenap is an independent open-source project and is **not affiliated with, sponsored by, or endorsed by Proxmox Server Solutions GmbH**. "Proxmox" is a trademark of its respective owner; it is used here only to describe compatibility.