Wake-on-LAN backup scheduler for Proxmox VE + PBS - let your backup servers sleep
See the codeYour Proxmox backup servers sleep. Joulenap wakes them, runs the backups, and tucks them back in.
Try the live demo · joulenap.com
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. 💤
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.
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.
config.yaml editor, plus an opt-in update check/metrics endpoint for Grafana (alert when a guest stops being backed up) — see docs/INTEGRATIONS.mdv1.4.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 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. 1.1 adds
an independent retention on the target of a sync route (plus PBS transfer-last /
remove-vanished), a no-progress timeout on task waits instead of a hard 6-hour cap, and
runs that can no longer be left hanging by a failed database write.
See 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 below before you pull.
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:
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://<host-ip>: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.
📖 Full guide: 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.
All settings live in config.yaml (see 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.
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.
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.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.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:
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). This is a way to
buy an evening, not a place to stay — please open an issue for whatever sent you back.
Joulenap can trigger backups and power machines on/off, so treat it as privileged:
docs/ARCHITECTURE.md.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.config.yaml holds secrets — keep its file permissions tight and out of version control.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.Licensed under the GNU Affero General Public License v3.0 (AGPL-3.0). See LICENSE.
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.
Wake-on-LAN backup scheduler for Proxmox VE + PBS - let your backup servers sleep
See the codeYour Proxmox backup servers sleep. Joulenap wakes them, runs the backups, and tucks them back in.
Try the live demo · joulenap.com
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. 💤
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.
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.
config.yaml editor, plus an opt-in update check/metrics endpoint for Grafana (alert when a guest stops being backed up) — see docs/INTEGRATIONS.mdv1.4.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 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. 1.1 adds
an independent retention on the target of a sync route (plus PBS transfer-last /
remove-vanished), a no-progress timeout on task waits instead of a hard 6-hour cap, and
runs that can no longer be left hanging by a failed database write.
See 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 below before you pull.
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:
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://<host-ip>: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.
📖 Full guide: 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.
All settings live in config.yaml (see 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.
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.
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.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.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:
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). This is a way to
buy an evening, not a place to stay — please open an issue for whatever sent you back.
Joulenap can trigger backups and power machines on/off, so treat it as privileged:
docs/ARCHITECTURE.md.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.config.yaml holds secrets — keep its file permissions tight and out of version control.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.Licensed under the GNU Affero General Public License v3.0 (AGPL-3.0). See LICENSE.
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.