Your Proxmox backup servers sleep. Joulenap wakes them, runs the backups, and tucks them back in.
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.