Skip to main content

A companion CLI for Runtipi: multi-remote rclone backups with per-remote retention, restores, app/core updates, VPS security hardening, and Tailscale setup.

Project description

runtipi-companion

A single Python CLI that extends and replaces the shell scripts scattered across Runtipi's docs (auto-backup, VPS security, server hardening) with one tool: runtipi-companion.

Built on top of, not instead of, runtipi-cli -- every action that Runtipi's own CLI already does well (start/stop, app update, appstore update) is a thin wrapper around it. What this tool adds:

  • Backups to multiple rclone remotes, each with its own retention. The official auto-backup-apps script only writes to local disk with one shared retention policy. This keeps that local behavior but adds any number of rclone remotes (B2, S3, Google Drive, SFTP, ...), each syncing independently with its own daily/weekly/monthly/yearly retention.
  • Restore from local disk or any configured remote, symmetric with the backup format. Every archive is verified (fully read back) right after creation, so a corrupt backup fails the backup run instead of surfacing at restore time.
  • Updates for individual apps, all apps (with an exclude list), app stores, and Runtipi core itself. By default a local pre-update snapshot of the affected apps is taken first (updates.backup_before), so every update is reversible via restore run.
  • A config wizard that runs on first start (or via config wizard): interviews you in the terminal and writes a validated config file.
  • A setup wizard for a fresh box: clone/verify the Runtipi install, locate runtipi-cli (it isn't on $PATH by default), run prepare/start, create backup directories, sanity-check rclone remotes.
  • Security hardening that applies the checklists from VPS security and Server hardening: SSH key-only + no root login, UFW, fail2ban, and an optional tailscale-only lockdown (see below). Pick what to apply from an interactive menu, or target items individually with flags.
  • Tailscale install + up, matching the VPN-only access pattern the VPS security guide recommends (don't open 80/443 publicly). security harden --tailscale-security goes further, based on this UFW+tailscale writeup: it switches SSH to tailscale ssh and restricts UFW to allow inbound traffic only on the tailscale0 interface, so the box is unreachable from the public internet entirely (only tailscale's own coordination port stays open).
  • One global YAML config for all of the above.

Safety model

Every command that changes system state (backups stop/start containers, security hardening edits sshd_config/firewall/installs packages, restore overwrites app data) defaults to --dry-run, which only prints what it would do. Add --apply to actually execute. SSH hardening additionally refuses to disable password authentication unless it finds an existing authorized_keys for root/the current user/$SUDO_USER (override with --force if you know what you're doing), and validates sshd_config with sshd -t before restarting -- rolling back automatically if the config is broken, so you can't lock yourself out of a remote box.

Install

From PyPI:

pipx install runtipi-companion

(Or without pipx:)

python3 -m venv /opt/runtipi-companion-venv
/opt/runtipi-companion-venv/bin/pip install /path/to/runtipi-companion
sudo ln -s /opt/runtipi-companion-venv/bin/runtipi-companion /usr/local/bin/runtipi-companion

Or, for local development:

pip install -e ".[dev]"
pre-commit install      # ruff lint + format on every commit
pytest

The end-to-end suite (tests/e2e/e2e.sh) exercises real rclone/tailscale binaries and applies changes for real, so run it in the provided Docker image rather than on your own machine:

docker build -f tests/e2e/Dockerfile -t runtipi-companion-e2e .
docker run --rm runtipi-companion-e2e

Requires rclone on $PATH for remote backups, and tailscale's installer handles its own binary. runtipi-cli does not need to be on $PATH -- runtipi-companion looks for it at <runtipi.path>/runtipi-cli automatically (set runtipi.cli_path in the config if yours lives elsewhere).

Managing several boxes (or provisioning unattended)? There's a thin Ansible playbook in deploy/ansible/ that installs the package, uploads a config, and runs the setup/hardening steps non-interactively.

Quickstart

On a first run (no config file anywhere), any command offers to launch the interactive config wizard, which interviews you and writes a validated config file. You can also start it explicitly:

runtipi-companion config wizard

Or go the manual route:

runtipi-companion config init --path ~/.config/runtipi-companion/config.yaml
$EDITOR ~/.config/runtipi-companion/config.yaml   # set runtipi.path, add rclone remotes

Then bootstrap the system and take a first backup:

runtipi-companion setup --apply                    # the setup wizard (same as 'setup wizard')
runtipi-companion setup rclone --apply             # install rclone + configure remotes
runtipi-companion setup services --apply           # systemd timers for automated backups

runtipi-companion backup run --type daily --apply
runtipi-companion backup list jellyfin

runtipi-companion security harden                  # interactive: pick what to harden
runtipi-companion security harden --all --apply    # or apply everything at once
runtipi-companion security harden --tailscale-security --apply   # VPN-only lockdown only

runtipi-companion setup tailscale --apply

Configuration

See runtipi-companion.example.yaml for every field with comments. Config is searched at /etc/runtipi-companion/config.yaml then ~/.config/runtipi-companion/config.yaml, or pass --config /path.

The file carries a version: schema number. When a companion update changes the config shape, every command prints a hint and

runtipi-companion config migrate            # preview steps + diff
runtipi-companion config migrate --apply    # back up original, rewrite

upgrades the file stepwise (v1 → v2 → ...). Files without version: count as v1. A config newer than the installed companion is refused with a pointer to self-update.

The part that matters most for the "individual retention per remote" ask:

backup:
  schedules:                      # local disk retention
    daily: { retention: 7 }
    weekly: { retention: 4 }
  remotes:
    - name: backblaze
      rclone_remote: "b2-runtipi:my-bucket/runtipi-backups"
      schedules:                  # THIS remote's own retention, independent of local
        daily: { retention: 14 }
        monthly: { retention: 12 }
    - name: gdrive
      rclone_remote: "gdrive:runtipi-backups"
      schedules:
        weekly: { retention: 4 } # only syncs weekly backups, keeps 4

A remote only receives backups for schedules it explicitly lists, and prunes itself independently of local disk and every other remote.

Local backups stay flat (<backup dir>/<store>/<app>/...); on every remote, each machine's backups sync into their own subfolder:

<remote>/<host_label>/<store>/<app>/<app>-<schedule>-<date>.tar.gz

backup.host_label defaults to the machine's hostname (the config wizard asks about it too). Several machines can share one remote bucket: each syncs and prunes only its own subtree, and never touches another host's backups.

Commands

runtipi-companion config   wizard|init|show|validate|migrate
runtipi-companion backup   run|list|remotes
runtipi-companion restore  run|list
runtipi-companion update   apps|core|appstores
runtipi-companion setup    wizard|services|rclone|fail2ban|tailscale   # bare 'setup' = wizard
runtipi-companion security harden|status
runtipi-companion tailscale status
runtipi-companion notify   test   # send a test message to every channel
runtipi-companion doctor        # health audit: pass/warn/fail, changes nothing
runtipi-companion self-update   # upgrade this tool from PyPI (pipx or pip)
runtipi-companion version

doctor audits the whole setup read-only -- runtipi install, docker, backup freshness, rclone remotes, and the VPS-security checklist (sshd effective config, UFW, fail2ban, tailscale) -- and exits non-zero if any check fails, so you can run it from monitoring.

Run runtipi-companion <group> <command> --help for full options.

Three commands have interactive TUI modes:

  • backup remotes — add/edit/remove/enable/disable rclone backup remotes in your config file from a menu, including per-schedule retention. Changes are validated before saving.
  • restore run with no arguments — pick the source (local disk or any configured remote), then the app, then the archive from a list of what actually exists, instead of typing filenames. Defaults to a dry-run preview; add --apply to restore for real.
  • security harden with no flags (or --interactive/-i) — checklist menu to pick any combination of SSH / UFW / fail2ban / tailscale-only lockdown. Flags (--ssh, --ufw, --fail2ban, --tailscale-security, --all) still work for scripting and skip the menu.

Automating backups

Either cron (matches the original guide):

sudo crontab -e
0 2 * * 2-7 /usr/local/bin/runtipi-companion backup run --type daily --apply
0 2 * * 1   /usr/local/bin/runtipi-companion backup run --type weekly --apply
0 3 1 * *   /usr/local/bin/runtipi-companion backup run --type monthly --apply

Or the bundled systemd timers, which add logging via journalctl and Persistent=true (a backup missed because the box was off runs as soon as it's back):

runtipi-companion setup services --apply           # daily,weekly,monthly by default
runtipi-companion setup services --schedules daily,weekly,monthly,yearly --apply

Notifications

Set notify.urls in the config to any number of apprise URLs -- ntfy, Discord, Slack, email, matrix, and ~80 other services:

notify:
  urls:
    - ntfy://ntfy.sh/my-topic
    - discord://webhook_id/webhook_token
  notify_on_success: false   # failures always notify by default
  notify_on_failure: true

Backup runs notify on failure (and optionally success) with proper success/failure message types -- ntfy users get priority/color out of the box. The old single notify.webhook_url (generic JSON POST) still works but is deprecated.

Verify your channels actually deliver before trusting them with failure alerts:

runtipi-companion notify test

sends a test message to every configured channel and reports each result (regular runs deliberately swallow notification errors; the test command does the opposite). Exits non-zero if any channel fails.

Restore

runtipi-companion backup list                      # every app's latest backup (shows app ids)
runtipi-companion restore list jellyfin --remote backblaze
runtipi-companion restore run jellyfin jellyfin-daily-2026-07-01.tar.gz --from-remote backblaze --apply

Omit --from-remote to restore from the local backup directory instead.

Restoring another machine's remote backups (migration path): pass --host with that machine's label alongside --from-remote, or use the interactive picker (restore run with no arguments), which lists every host it finds on the chosen remote. An empty remote listing also prints which other host labels exist.

runtipi-companion backup list --remote backblaze --host old-vps
runtipi-companion restore run jellyfin jellyfin-daily-2026-07-01.tar.gz \
    --from-remote backblaze --host old-vps --apply

Limitations / things to know

  • The backup format is this tool's own (tar.gz of app/app-data/user-config, same layout as the original bash script), not runtipi-cli app backup's native format -- they aren't interchangeable. runtipi-cli app backup is still exposed via the RuntipiCLI wrapper for scripting if you want it.
  • Remote pruning parses rclone lsf -R output; very unusual remote path layouts (colons or the schedule name embedded oddly in a path segment) could confuse the app-name grouping. Test with --dry-run after your first real sync to a new remote before trusting the retention prune.
  • Security hardening covers the concrete steps from Runtipi's own docs (SSH keys, UFW, fail2ban). It's not a substitute for reading those docs once yourself.

Ideas for later (not built yet)

Things worth considering if this becomes your daily driver:

  • Backup encryption (age or gpg) before upload, since remotes are third-party cloud storage.
  • --json output on every command for monitoring integration (doctor's exit code already works for simple alerting).
  • Fleet mode: point one config at multiple Runtipi hosts (e.g. via SSH) for centralized backup/update status across boxes. Provisioning many boxes is already covered by the ansible playbook in deploy/ansible/; this would add day-2 status. Already graduated from this list: pre-update snapshots, backup integrity verification, doctor, self-update, and richer notifications (apprise).

Happy to build any of these next -- say the word.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

runtipi_companion-1.3.3.tar.gz (65.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

runtipi_companion-1.3.3-py3-none-any.whl (62.6 kB view details)

Uploaded Python 3

File details

Details for the file runtipi_companion-1.3.3.tar.gz.

File metadata

  • Download URL: runtipi_companion-1.3.3.tar.gz
  • Upload date:
  • Size: 65.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for runtipi_companion-1.3.3.tar.gz
Algorithm Hash digest
SHA256 e4d349c8e2ce61908f8f13becbc7c8615f15211f56271ac51cc6b3d2fea78b85
MD5 e4bc86978e639d15ef2d62a26f87c405
BLAKE2b-256 2c55666ac5713e7276fb9d91e7684b8f760c930e603732a2a76e363fb2a15297

See more details on using hashes here.

Provenance

The following attestation bundles were made for runtipi_companion-1.3.3.tar.gz:

Publisher: release.yml on EckPhi/runtipi-companion

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file runtipi_companion-1.3.3-py3-none-any.whl.

File metadata

File hashes

Hashes for runtipi_companion-1.3.3-py3-none-any.whl
Algorithm Hash digest
SHA256 d21409646e30cdcd50cef787943296986304f66f5f465e6433f5fcb7e2eb8c09
MD5 88ba601309012f83b6b0bb518ed901db
BLAKE2b-256 4d4d4a5a752cecea183527350649464a2dd8c7d652a3d44a7597fc2250508f18

See more details on using hashes here.

Provenance

The following attestation bundles were made for runtipi_companion-1.3.3-py3-none-any.whl:

Publisher: release.yml on EckPhi/runtipi-companion

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page