- Shell 100%
| example.conf | ||
| install.sh | ||
| jabs-completion.bash | ||
| jabs.sh | ||
| postgres.sh | ||
| README.md | ||
JABS
JABS stands for Just Another Backup Service.
JABS is a small backup orchestrator for Linux systems. Its job is not to be a complete backup platform on its own, and it is not trying to hide what it is doing behind a large framework. Instead, it gives you a simple command, a small set of config files, and a predictable execution model for running backups from a machine that already knows how to reach the data you care about.
At a practical level, JABS does this:
- reads a list of backup targets from a config file
- looks at each target's URI or path
- decides which backup backend should handle that target
- runs the matching backup logic
- writes logs per target
- uses lock files so the same target path is not processed concurrently
That means you can define a PostgreSQL database, a filesystem path, or other future backend types in one place and run them through the same entrypoint. JABS is especially suited to environments where you want a Unix-friendly tool that can be installed as a single executable, configured with plain text files, and scheduled externally with cron, systemd timers, or your orchestration tooling.
The repository currently ships with:
- the main
jabsrunner - an installer that bundles the project into one executable script
- a PostgreSQL backup backend
- an example targets file
Table of Contents
- What JABS Does
- How JABS Works
- Current Capabilities
- Installation
- Filesystem Layout
- Configuration
- Targets File
- Execution Modes
- Security Model
- Backend Detection
- Running JABS
- Logging and Locking
- Modules and Extensibility
- Current Limitations
What JABS Does
JABS exists to answer a simple operational need: "I have several things that need backing up, they are not all the same kind of thing, and I want one small Linux command that can process them consistently."
Instead of giving every backup target its own ad hoc shell script, JABS lets you describe targets in a single file with a common structure. Each entry says what the target is, where it lives, how it should be reached, where its restic output should go, which SSH key should be used, and whether it should run natively, in a local Docker container, or in Docker Swarm.
JABS then walks through those entries one by one and performs the backup for each target using the backend that matches the target URI.
In other words, JABS is the coordinator. The module code does the backend-specific work.
How JABS Works
The high-level lifecycle of a jabs run is:
- JABS starts and loads
/etc/jabs/config.conf. - It ensures the configured lock and log directories exist.
- It reads
/etc/jabs/targets.confline by line. - For each valid target entry, it detects the backend from the URI.
- It loads the corresponding backend module.
- It creates a lock file tied to the target's
restic_path. - It runs the selected backend handler for the chosen execution mode.
- It appends output to that target's log file.
This model keeps the runtime straightforward:
- one executable
- one main config file
- one targets file
- backend-specific functions for actual backup work
Current Capabilities
At the moment, this repository provides:
- a Bash-based main runner
- single-file installation into
/usr/local/bin/jabs - automatic creation of
/etc/jabsand/etc/jabs/targets.conf - PostgreSQL backup support
- native mode support
- local Docker mode support
- Docker Swarm mode support
The code already includes backend detection rules for MariaDB and MySQL URIs, MongoDB, and filesystem paths, but those handlers are not yet shipped in this repository. Right now, PostgreSQL is the only backend implementation included here.
Installation
Install JABS as root:
sudo ./install.sh
The installer does not "compile" JABS in the traditional binary sense. Since JABS is Bash, installation is simply the process of building a single runnable script and placing it somewhere standard in the filesystem.
More specifically, the installer:
- creates
/etc/jabsif it does not exist - creates
/etc/jabs/config.confif it does not exist - creates
/etc/jabs/targets.confif it does not exist - creates
/etc/jabs/sshif it does not exist - bundles
jabs.shtogether with the shipped module files - installs the bundled result as
/usr/local/bin/jabs - installs Bash completion to
/etc/bash_completion.d/jabs - marks the installed file executable
After installation, jabs can be called directly like any other normal Linux command:
jabs
Filesystem Layout
JABS uses a simple Linux-style layout:
/usr/local/bin/jabs
/etc/jabs/config.conf
/etc/jabs/targets.conf
/etc/jabs/ssh/
/etc/jabs/runtime-state.json
Optional external modules may also live under:
/etc/jabs/modules/
What each path is for:
/usr/local/bin/jabs: the installed executable entrypoint/etc/jabs/config.conf: global runtime configuration/etc/jabs/targets.conf: list of backup targets to process/etc/jabs/ssh/: service-managed SSH key directory/etc/jabs/runtime-state.json: learned per-target runtime and timeout state/etc/jabs/modules/: optional out-of-tree backend modules
Even though the installer produces a bundled single-file executable, the runtime still supports loading extra modules from /etc/jabs/modules/ if you want to extend it later without rebuilding the installed file.
Configuration
JABS expects a global configuration file at:
/etc/jabs/config.conf
This file is sourced by Bash, so it should contain shell variable assignments.
A minimal example looks like this:
LOCK_BASE="/var/lock/jabs"
LOG_BASE="/var/log/jabs"
SSH_USER=""
SSH_PORT="22"
RESTIC_BASE="sftp:backup@example.com:/backups"
# Optional:
# RESTIC_PASSWORD_FILE="/etc/jabs/restic-password"
# RESTIC_PASSWORD="replace-me"
# JABS_BACKUP_TIMEOUT_DEFAULT="900"
# JABS_BACKUP_TIMEOUT_STEP="600"
# JABS_BACKUP_TIMEOUT_MAX="7200"
# JABS_BACKUP_TIMEOUT_WARN_PCT="80"
# JABS_BACKUP_TIMEOUT_EXPAND_PCT="92"
# JABS_BACKUP_TIMEOUT_CRITICAL_PCT="90"
# JABS_BACKUP_RUNTIME_WINDOW="5"
# JABS_SWARM_LOCK_WAIT_TIMEOUT="60"
DOCKER_IMAGE_ALPINE="alpine:latest"
On a first install, install.sh creates this file automatically with the default template shown above. If the file already exists, it is left untouched.
Configuration Variables
LOCK_BASE
Directory where JABS stores lock files. Locks are used to prevent multiple runs from handling the same restic_path at the same time.
LOG_BASE
Directory where JABS writes per-target log files.
SSH_USER
Optional SSH username for the restic transport. If left empty, JABS warns and falls back to the user running jabs.
SSH_PORT
SSH port used for the SSH connection that restic opens to the remote repository endpoint derived from RESTIC_BASE.
RESTIC_BASE
Base repository location passed to restic. Each target appends its own restic_path under this base. JABS also derives the SSH target host from this value for the current PostgreSQL backend.
RESTIC_PASSWORD_FILE
Optional explicit file path containing the restic repository password.
RESTIC_PASSWORD
Optional inline restic repository password.
JABS_BACKUP_TIMEOUT_DEFAULT
Default backup timeout in seconds for new targets. Defaults to 900.
JABS_BACKUP_TIMEOUT_STEP
Per-target timeout increase step in seconds when a target repeatedly gets too close to its limit. Defaults to 600.
JABS_BACKUP_TIMEOUT_MAX
Maximum learned timeout in seconds for any target. Defaults to 7200.
JABS_BACKUP_TIMEOUT_WARN_PCT
Warn threshold, as a percentage of the current target timeout. Defaults to 80.
JABS_BACKUP_TIMEOUT_EXPAND_PCT
Auto-expand threshold, as a percentage of the current target timeout. Defaults to 92.
JABS_BACKUP_TIMEOUT_CRITICAL_PCT
Critical review threshold, as a percentage of the maximum timeout. Defaults to 90.
JABS_BACKUP_RUNTIME_WINDOW
Number of recent successful runs used for the rolling mean. Defaults to 5.
JABS_SWARM_LOCK_WAIT_TIMEOUT
Optional timeout in seconds for waiting on a Swarm backup container to acquire its shared lock file. Defaults to 60.
DOCKER_IMAGE_ALPINE
Container image used by the Docker-based PostgreSQL backup handlers.
Why RESTIC_BASE and SSH_PORT Matter
In the shipped PostgreSQL backend, JABS does not simply write backups to an arbitrary local directory. It runs restic with a server-side remote command path over SSH.
In practice, the current backend uses a command pattern equivalent to:
restic -o rclone.program="ssh -i ... -p $SSH_PORT <derived-from-RESTIC_BASE>" -o rclone.args="" ...
That means the backup client talks to a controlled remote endpoint through SSH, and the remote side is expected to expose only the backup command path you intend it to expose.
Restic Password Resolution
JABS resolves the restic password in this order:
RESTIC_PASSWORD_FILERESTIC_PASSWORD/etc/jabs/restic-password
If none of those are available, the backup run fails.
For a Linux service, the recommended default is to store the password in:
/etc/jabs/restic-password
and keep that file readable only by root.
Important Runtime Expectation
The generated default config is only a starting point. You should edit /etc/jabs/config.conf to match your actual environment before relying on JABS in production.
Targets File
JABS reads targets from:
/etc/jabs/targets.conf
Each non-empty, non-comment line represents one backup target.
Expected format:
name uri network restic_path ssh_key docker_mode
Field Reference
name
A human-readable identifier for the backup target. This is also used in log messages and temporary naming.
uri
The source location. This determines which backend module JABS selects. For example, a postgres://... URI selects the PostgreSQL backend.
network
The Docker network name used for containerized execution modes. If the target does not need a network, you can use a placeholder like none.
restic_path
The destination path segment used under your configured RESTIC_BASE. JABS also uses this field when creating lock and log file names.
ssh_key
The SSH key identifier used by the backend implementation.
When this value is just a name, JABS resolves it in this order:
${HOME}/.ssh/<ssh_key>/etc/jabs/ssh/<ssh_key>- absolute path given directly in the field
If none of those exist, containerized modes treat the value as a Docker secret name.
If you want to force secret handling explicitly, use:
secret=<secret_name>
Examples:
backup-key-main-> try local file lookup first, then fall back to a secret namedbackup-key-mainsecret=backup-key-main-> skip file lookup and use the Docker secret directly/etc/jabs/ssh/backup-key-main-> use that exact file
In containerized modes, JABS injects one of these into the container:
SSH_KEYwhen the key was resolved outside from a real fileSSH_KEY_FILEwhen the key is provided through a Docker secret
Native mode still requires a real key file on the daemon side.
docker_mode
Execution selector:
0: native execution on the host1: local Docker container execution2: Docker Swarm service execution
Example
# name uri network restic_path ssh_key docker (0=no,1=local,2=swarm)
auth-db postgres://appuser:app-password@postgres-db:5432/appdb app-net auth-db backup-key-auth 2
web-db mysql://webuser:web-password@mysql-db:3306/webapp web-net web-db backup-key-web 2
app-logs /var/log/myapp none app-logs backup-key-logs 1
local-files /srv/data none local-files backup-key-files 0
A sample file is included at example.conf.
Execution Modes
JABS supports three execution modes at the target level.
Native Mode
Native mode is selected with:
docker_mode = 0
In this mode, the backup logic runs directly on the host system. This is the simplest mode and assumes all required tools are already installed on the machine running JABS.
Local Docker Mode
Local Docker mode is selected with:
docker_mode = 1
In this mode, the backend runs inside a local Docker container attached to the configured Docker network.
Docker Swarm Mode
Docker Swarm mode is selected with:
docker_mode = 2
In this mode, the backend is executed as a Docker Swarm service.
Swarm mode is constrained back onto the same node that launched the backup. This allows JABS to keep using kernel-backed local file locks instead of adding a separate distributed lock service.
The Swarm backup container receives a bind mount of the JABS lock directory and acquires the target lock itself inside the container. The daemon then waits for that lock to appear, watches for it to be released, and removes the Swarm service when the backup is done.
This keeps the locking model simple:
- the container owns the
flock - the kernel releases that lock automatically if the container exits or crashes
- no Redis or other coordination service is required
The daemon waits up to JABS_SWARM_LOCK_WAIT_TIMEOUT seconds for the container to acquire the lock after the service is created.
When the configured network name is a logical stack network name, JABS will look for matching real Swarm network names. If multiple candidates exist, it tries them one by one. A candidate can only be ignored in interactive mode when an operator explicitly confirms it. jabs --daemon fails hard instead of silently skipping networks.
Security Model
JABS is designed around the idea that backups should remain protected even if the machine running JABS is later compromised.
The intended model is:
- JABS runs on the primary machine or orchestration node
- backup data is sent to a remote backup host
- restic is used as the backup engine
- the remote path is reached through an SSH-based
rclone.program/ forced-command setup - the backup side is configured in append-only mode
The important point is that the source machine should not have unrestricted delete or repository-admin rights on the backup server.
What This Protects Against
If the main application server is compromised, an attacker may still be able to run jabs or read local config and keys available on that host. But if the remote backup side is set up correctly, that does not automatically give the attacker the ability to rewrite history or destroy existing snapshots.
With a restricted remote command and append-only enforcement on the backup side:
- the source host can add new backup data
- the source host cannot freely prune, rewrite, or delete existing backup state
- old snapshots remain immutable unless someone with separate administrative access changes that policy
This is the security property you want from a backup system: compromise of the production host should not mean immediate loss of the backups.
Important Boundary
That immutability guarantee does not come from JABS alone. It depends on how the remote backup side is configured.
For the model to hold, the backup server must enforce:
- a restricted SSH command path, such as the forced-command flow used by the current backend
- append-only permissions for the backup client identity
- separate administrative credentials for destructive operations such as pruning or repository maintenance
If the same credentials available on the source machine also have full administrative control over the repository, then backups are not meaningfully immutable.
Practical Interpretation
So the correct way to think about JABS is:
- JABS is the orchestrator
- restic is the backup engine
- the remote server policy is what provides immutability
When deployed that way, the repository becomes append-only from the perspective of the production host, which is exactly the behavior you want for high-trust backup retention.
Example Remote SSH Setup
One practical way to enforce this on the backup server is to pin the client's SSH key to a forced rclone serve restic command with append-only mode enabled.
Actual example:
File:
/home/backups/.ssh/authorized_keys
Line to add:
command="rclone serve restic --stdio --append-only /home/backups/backuprepo",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty ssh-ed25519 add-a-random-key/CegyieMNRUkJzxOo/qpyzxs5V3 somecomment
What this does:
- forces that key to run only
rclone serve restic - exposes the repository over stdio instead of a general-purpose shell
- enables append-only mode on the remote side
- disables port forwarding, X11 forwarding, agent forwarding, and PTY allocation for that key
Operationally, this means the key used by JABS can push backup data into /home/backups/backuprepo, but it does not receive a normal interactive SSH session. Combined with append-only enforcement, this is the pattern that keeps previously written backup data protected from routine compromise of the source host.
If you use this pattern, your SSH_PORT, key material, and RESTIC_BASE values should all be aligned with that remote repository layout and access model.
Backend Detection
JABS chooses a backend by inspecting the target URI.
The current detection rules are:
postgres://->postgresmysql://ormariadb://->mariadbmongodb://->mongodb- anything that is not a recognized URI scheme is treated as a filesystem path and maps to
files
This means the target definition itself tells JABS what kind of thing it is backing up. You do not need a separate "type" field as long as the URI format is consistent.
For filesystem targets, JABS intentionally accepts plain POSIX-style paths broadly. That includes values like:
/var/log/app./backups/datadata/archivedata
In practice, JABS treats anything that does not match a known ...:// scheme as a filesystem path. The unknown result is reserved for values that still look like a URI, but use an unsupported scheme.
Important detail: detection support does not automatically mean implementation support. At the moment, this repository only ships the PostgreSQL backend module.
Running JABS
Once installed and configured, running JABS is intentionally simple:
jabs
This shows the command help.
To run backups explicitly, use:
jabs run
By default, jabs runs in interactive operator mode:
- backend output is shown on the terminal
- the same output is still written to the per-target log file
- safety prompts are allowed when JABS needs operator confirmation
- high-level progress messages are shown while JABS is working
For unattended runs, use:
jabs --daemon
In daemon mode, JABS is non-interactive and writes only to the configured log files.
For detailed troubleshooting, use:
jabs --debug
Debug mode keeps interactive behavior but also prints internal execution decisions such as resolved network candidates, lock handling, and backend launch attempts.
Additional commands:
jabs list
jabs ps
jabs listshows configured targetsjabs psshows currently active runs based on held lock files
If Bash completion is enabled on the system, jabs command and option completion is installed automatically.
JABS also keeps learned per-target runtime state in:
/etc/jabs/runtime-state.json
That state is used to:
- track recent successful runtimes per target
- compute a rolling mean over the last few runs
- warn in interactive mode when a target is getting close to its current timeout
- auto-expand only that target's timeout in fixed steps when it repeatedly runs too close to the limit
- raise a critical warning when a target is getting too close to the configured maximum timeout and should be reviewed by a human
If a daemon run fails, JABS writes a failure flag file at:
/etc/jabs/daemon-failure.flag
The next interactive jabs run prints a large warning and shows the first lines of that file so the failure is hard to miss. The warning stays until you remove or rename the flag file after investigation.
JABS processes the configured targets sequentially in the order they appear in /etc/jabs/targets.conf.
A normal operational flow is usually:
- install JABS
- create
/etc/jabs/config.conf - define targets in
/etc/jabs/targets.conf - run
jabsmanually to verify behavior - schedule it with cron, a systemd timer, or your preferred orchestration layer
Logging and Locking
JABS keeps runtime behavior explicit and inspectable.
Logging
For each target, JABS writes to a log file under:
LOG_BASE/<restic_path>.log
These logs include start and completion messages as well as backend command output.
Locking
For each target, JABS creates a lock file under:
LOCK_BASE/<restic_path>.lock
The lock is acquired with flock. This prevents concurrent work on the same logical backup path, which is useful when JABS is triggered by schedule overlap or multiple invocations.
Modules and Extensibility
JABS is structured around backend modules.
The main runner is responsible for orchestration:
- reading config
- reading targets
- selecting a backend
- managing locks
- writing logs
The backend module is responsible for the backup mechanics for that backend.
The repository currently includes:
At install time, shipped modules are bundled into the installed executable. In addition, JABS can still source external modules from /etc/jabs/modules/ if you want to add new backend handlers without changing the repository layout.
Current Limitations
The current codebase is intentionally small, but it has some important boundaries that should be understood clearly:
- targets are processed sequentially - multithreading/multicontainers coming up
- backend behavior depends on external tools such as
pg_dump,restic,docker,ssh, and related runtime setup - checks for these will be added asap