A Bash script that backs up the SQLite database from one or more 3x-ui panel installations via the official REST API. A JSON configuration file defines the list of endpoints, credentials, backup destination, and retention policy.
Find a file
Paul Git 710535610d
Version 3.0.0: XDG paths, optional destination_folder, bug fixes
- Switch to XDG Base Directory layout; config under ~/.config/,
  backups under ~/.local/share/. No root or sudo required.
- Make destination_folder optional (falls back to XDG default).
- Make -c optional (falls back to XDG default config path).
- Add set -o errtrace for ERR trap inheritance in functions.
- Add expand_tilde_path() helper; apply to all three path types
  (config_file, token_file, destination_folder). Handles ~/foo and
  bare ~; leaves ~username/ unexpanded for a clear error.
- Guard HOME early with :? so a missing HOME aborts with a helpful
  message before the readonly XDG globals are evaluated.
- Fix log_debug-before-cron-buffering race: move
  enable_cron_output_buffering above the default-config fallback.
- Promote config/destination fallback notices from log_debug to
  log_info so they appear without --verbose.
- Add ep_name parameter to download_database; TLS-disabled warning
  now includes [ep_name] prefix like all other log messages.
- Warn and coerce unrecognised tls_verify values (e.g. JSON null)
  to true rather than passing "null" silently to curl.
- Guard against whitespace-only destination_folder values.
- Fix two-step readonly SCRIPT_NAME to single-statement form.
- Update AGENTS.md: trim to agent-relevant rules, drop interactive
  guidelines and hard style limits.
- Remove CLAUDE.md (superseded by AGENTS.md).
- Update README and example.config.json for new defaults.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-21 22:00:46 +07:00
.gitignore Initial public release 2026-04-03 17:06:05 +08:00
3x-ui-db-backup.sh Version 3.0.0: XDG paths, optional destination_folder, bug fixes 2026-06-21 22:00:46 +07:00
AGENTS.md Version 3.0.0: XDG paths, optional destination_folder, bug fixes 2026-06-21 22:00:46 +07:00
example.config.json Version 3.0.0: XDG paths, optional destination_folder, bug fixes 2026-06-21 22:00:46 +07:00
example.token.json Add example.token.json for Bearer token auth 2026-05-12 19:33:05 +08:00
LICENSE Initial public release 2026-04-03 17:06:05 +08:00
README.md Version 3.0.0: XDG paths, optional destination_folder, bug fixes 2026-06-21 22:00:46 +07:00

3x-ui Database Backup Script

Version

A Bash script that backs up the SQLite database from one or more 3x-ui panel installations via the official REST API. A JSON configuration file defines the list of endpoints, API tokens, backup destination, and retention policy.

The script follows the XDG Base Directory Specification: configuration lives under ~/.config/3x-ui-db-backup/, backups under ~/.local/share/3x-ui-db-backup/, and the script installs to ~/.local/bin/3x-ui-db-backup. No root or sudo is required.


Table of Contents


Features

  • Backs up the SQLite database of multiple 3x-ui installations in a single run
  • Uses the official 3x-ui REST API — no SSH or direct filesystem access required
  • Bearer token authentication — a static API token is sent via the Authorization: Bearer header; no login round-trip is required
  • Separate token files — tokens can be isolated from the main config file, following the same pattern used by MySQL, PostgreSQL, the AWS CLI, and rclone
  • Runtime permissions enforcement — the script refuses to use a token file that is readable by group or other
  • Per-endpoint retention policy — automatically deletes the oldest backups once the configured limit is exceeded
  • Custom webBasePath support — works with panels configured to use a non-default URL prefix
  • TLS verification toggle — can skip certificate verification for self-signed certificates
  • Cron mode — suppresses all output on success so cron does not send email for routine runs; emits the full captured log on failure
  • Verbose / debug mode for troubleshooting
  • Timestamped backup filenames for easy identification
  • Graceful partial failure — if one endpoint fails, the script continues with the remaining endpoints and reports a summary at the end
  • Automatic cleanup of temporary files on exit, including on error

Requirements

The following tools must be available on the system running the script:

Tool Purpose
bash Shell interpreter (version 4.0 or later)
curl HTTP requests to the 3x-ui API
jq Parsing the JSON configuration file and token files
file Sanity-checking the downloaded database file
stat Reporting the size of the downloaded file and checking file permissions
mktemp Creating temporary files for the cron output buffer
date Generating timestamps for backup filenames
find / sort Listing and ordering backups for rotation

All of these are standard tools available on any modern Linux distribution. On Debian/Ubuntu, you may need to install jq if it is not already present:

sudo apt-get install jq

On RHEL/CentOS/Fedora:

sudo dnf install jq

Installation

The script is installed at the user level — no root or sudo required.

  1. Ensure ~/.local/bin is on your PATH (so the script can be invoked by name after install). Add this to your shell rc file (~/.bashrc, ~/.zshrc, etc.) if not already present:

    export PATH="${HOME}/.local/bin:${PATH}"
    

    Reload the rc file or start a new shell afterwards.

  2. Create the install directory and download the script (the .sh extension is dropped on install):

    mkdir -p ~/.local/bin
    curl -o ~/.local/bin/3x-ui-db-backup \
        https://code.paulg.it/paulgit/3x-ui-db-backup/raw/branch/main/3x-ui-db-backup.sh
    

    Or clone the repository and copy/symlink the script manually:

    git clone https://code.paulg.it/paulgit/3x-ui-db-backup.git
    cd 3x-ui-db-backup
    cp 3x-ui-db-backup.sh ~/.local/bin/3x-ui-db-backup
    
  3. Make the script executable:

    chmod +x ~/.local/bin/3x-ui-db-backup
    
  4. Create the configuration directory:

    mkdir -p ~/.config/3x-ui-db-backup
    chmod 700 ~/.config/3x-ui-db-backup
    
  5. Create your main configuration file (see Configuration below):

    nano ~/.config/3x-ui-db-backup/config.json
    
  6. Create a token file for each endpoint (see API Token Management below):

    nano ~/.config/3x-ui-db-backup/vps-london.token.json
    
  7. Lock down all configuration and token files:

    chmod 600 ~/.config/3x-ui-db-backup/config.json
    chmod 600 ~/.config/3x-ui-db-backup/*.token.json
    
  8. The backup destination directory is created automatically on the first run (default: ~/.local/share/3x-ui-db-backup/). To create it in advance:

    mkdir -p ~/.local/share/3x-ui-db-backup
    chmod 700 ~/.local/share/3x-ui-db-backup
    

Configuration

The script is driven by a JSON configuration file, read by default from ~/.config/3x-ui-db-backup/config.json (override with -c <file>). API tokens are kept in separate per-endpoint token files (recommended) rather than inline in the main config.

Main Configuration File

{
  "endpoints": [
    {
      "name":       "vps-london",
      "base_url":   "https://vps1.example.com:2053",
      "base_path":  "/",
      "token_file": "~/.config/3x-ui-db-backup/vps-london.token.json",
      "keep_backups": 7,
      "tls_verify": true
    },
    {
      "name":       "vps-frankfurt",
      "base_url":   "https://vps2.example.com:54321",
      "base_path":  "/mySecretPath/",
      "token_file": "~/.config/3x-ui-db-backup/vps-frankfurt.token.json",
      "keep_backups": 14,
      "tls_verify": false
    },
    {
      "name":       "home-lab",
      "base_url":   "http://192.168.1.50:2053",
      "base_path":  "/",
      "token_file": "~/.config/3x-ui-db-backup/home-lab.token.json",
      "keep_backups": 3,
      "tls_verify": true
    }
  ]
}

destination_folder is optional and omitted above; backups are then written to the default location (~/.local/share/3x-ui-db-backup/). To override it, add a top-level destination_folder field:

{
  "destination_folder": "~/backups/3x-ui",
  "endpoints": [ ... ]
}

Main Configuration Field Reference

Top-level fields

Field Type Required Description
destination_folder string Path to the root backup directory. Sub-directories are created automatically per endpoint. Can be absolute or relative. Defaults to ~/.local/share/3x-ui-db-backup/.
endpoints array List of 3x-ui panel endpoints to back up. Must contain at least one entry.

Per-endpoint fields

Field Type Required Default Description
name string A short, filesystem-safe label for the endpoint. Used as the sub-directory name and as a prefix in backup filenames. Use only letters, numbers, hyphens, and underscores.
base_url string The scheme, hostname or IP address, and port of the panel. No trailing slash. Examples: https://host:2053, http://192.168.1.1:8080
base_path string "/" The webBasePath configured in the panel. Must start and end with /. If you have not set a custom path in the panel, use "/".
token_file string * Path to a separate JSON file containing api_token. Must have permissions 600 and be owned by the user running this script. Takes precedence over an inline api_token.
api_token string * Static Bearer token for the 3x-ui API. Inline — less secure. Use token_file instead.
keep_backups integer 7 The number of most-recent backup files to retain for this endpoint. Must be a positive integer. Older files are deleted automatically after each successful backup.
tls_verify boolean true Set to false to disable TLS certificate verification. Useful when the panel uses a self-signed certificate. Not recommended for production use.

* Either token_file or an inline api_token must be provided for each endpoint.

Finding Your base_path

The webBasePath is the URL prefix configured in the 3x-ui panel settings. To check the current value, run the following on the server hosting the panel:

x-ui settings

If the path is set to /, the database API URL will be:

https://host:2053/panel/api/server/getDb

If the path is set to /mySecretPath/, the database API URL will be:

https://host:2053/mySecretPath/panel/api/server/getDb

API Token Management

Storing the API token inline in the main configuration file is convenient but not recommended for production use. The script supports a more secure pattern — separate token files — which follows the same approach used by tools such as MySQL (.my.cnf), PostgreSQL (.pgpass), the AWS CLI (~/.aws/credentials), and rclone.

Obtaining Your API Token

To obtain a static API token, log in to the 3x-ui panel web UI and navigate to the API settings section. Copy the token and store it in a token file as described below.

Token File Format

Each token file is a minimal JSON file containing only the API token for one endpoint:

{
  "api_token": "your-static-api-token-here"
}

Create and secure a token file for each endpoint:

# Create the token file
nano ~/.config/3x-ui-db-backup/vps-london.token.json

# Enforce owner read/write only
chmod 600 ~/.config/3x-ui-db-backup/vps-london.token.json

⚠️ The script enforces that token files have permissions 600 at runtime. It will refuse to use a token file that is readable by group or other, and will skip that endpoint with an error.

Why Separate Token Files Are More Secure

Property Inline in config Separate token file
Token isolated from config
Can be owned by a dedicated service account
Permissions enforced at runtime (600 check)
Config file safe to share or version-control
Token readable by group or other ⚠️ depends on config perms blocked by script

Token Precedence Rules

Scenario Behaviour
token_file present and valid (600) Token loaded from file; inline api_token ignored
token_file present but permissions are not 600 Endpoint skipped with an error
token_file absent, inline api_token present Inline token used; a [WARN] is logged
Neither token_file nor inline api_token provided Endpoint skipped with an error

If a token file is not feasible, the token can be provided inline in the main config. A [WARN] will be logged each time this is used.

{
  "endpoints": [
    {
      "name":       "vps-london",
      "base_url":   "https://vps1.example.com:2053",
      "base_path":  "/",
      "api_token":  "your-static-api-token-here",
      "keep_backups": 7,
      "tls_verify": true
    }
  ]
}

Usage

Usage: 3x-ui-db-backup [OPTIONS] [-c <config_file>]

Options:
  -c, --config <file>   Path to the JSON configuration file.
                        Default: ~/.config/3x-ui-db-backup/config.json
  -v, --verbose         Enable verbose/debug output
      --cronmode        Suppress all output on success; emit full captured
                        log on failure (for use with cron). Compatible
                        with -v / --verbose.
  -h, --help            Show this help message and exit
      --version         Print version and exit

Basic Usage

When invoked with no options, the script reads the default config file and writes backups to the default destination folder:

3x-ui-db-backup

To use a specific config file:

3x-ui-db-backup -c ~/.config/3x-ui-db-backup/config.json

Verbose Mode

Verbose mode prints additional debug information, including the exact URLs being contacted and per-file rotation details.

3x-ui-db-backup --verbose

Cron Mode

--cronmode buffers all script output during execution. On success, the buffer is silently discarded — the run produces no output, so cron does not send an email. On failure, the full captured log is written to stdout so cron can deliver it to the operator.

# Silent on success, full log on failure:
3x-ui-db-backup --cronmode

# Silent on success, verbose log on failure:
3x-ui-db-backup --cronmode --verbose

--cronmode and --verbose are fully independent and can be combined freely. See Scheduling with Cron for recommended crontab entries.

Exit Codes

Code Meaning
0 All backups completed successfully
1 One or more backups failed (partial success is still reported)
2 Fatal error — bad arguments, missing dependencies, or unreadable / invalid config file

Backup File Structure

The script creates one sub-directory per endpoint inside the destination_folder (default: ~/.local/share/3x-ui-db-backup/). Each backup file is named using the endpoint's name and a timestamp.

~/.local/share/3x-ui-db-backup/
├── vps-london/
│   ├── vps-london_20260401_020000.db
│   ├── vps-london_20260402_020000.db
│   └── vps-london_20260403_020000.db
├── vps-frankfurt/
│   ├── vps-frankfurt_20260327_020000.db
│   ├── ...
│   └── vps-frankfurt_20260403_020000.db
└── home-lab/
    └── home-lab_20260403_020000.db

Filename format: <name>_YYYYMMDD_HHMMSS.db

The timestamp reflects the date and time on the machine running the script, in the local system timezone.


Retention / Rotation

After each successful download, the script counts all .db files in the endpoint's backup directory. If the count exceeds keep_backups, the oldest files (by modification time) are deleted until only keep_backups files remain.

Example: with "keep_backups": 7 and 8 files present, the oldest 1 file is deleted, leaving exactly 7.

⚠️ Rotation only runs after a successful download. If a backup fails, no files are deleted.


Scheduling with Cron

Run the backup from your own user crontab — no root or service account required.

crontab -e

Using --cronmode is the simplest and cleanest approach for cron jobs. The script produces no output on success, so cron sends no email. On failure, cron automatically emails the full output to the local MAILTO address.

0 2 * * * ${HOME}/.local/bin/3x-ui-db-backup --cronmode

To receive verbose output in failure emails (recommended for easier diagnosis):

0 2 * * * ${HOME}/.local/bin/3x-ui-db-backup --cronmode --verbose

To use a non-default config file:

0 2 * * * ${HOME}/.local/bin/3x-ui-db-backup -c /path/to/config.json --cronmode

Alternative: Redirect to a Log File

If you prefer to keep a persistent log of all runs regardless of outcome, redirect output to a file under ~/.local/state/ (the XDG location for application state). With this approach, cron will send an email on every run (since there will always be stdout output), so you may want to suppress that with >/dev/null 2>&1 and rely solely on the log file.

0 2 * * * ${HOME}/.local/bin/3x-ui-db-backup >> ${HOME}/.local/state/3x-ui-db-backup/backup.log 2>&1

Create the log directory in advance:

mkdir -p ~/.local/state/3x-ui-db-backup

Log Rotation

User-state logs are not handled by logrotate by default. The simplest approach is to use --cronmode (no persistent log at all). If you do keep a log file, rotate it manually or with a small cron entry such as:

# Keep the last 8 weeks of logs
find ~/.local/state/3x-ui-db-backup -name 'backup.log.*' -mtime +56 -delete

Alternatively, use logrotate with a user configuration if your distribution supports logrotate --state ~/.local/state/logrotate.status.


Logging

All output follows this format:

[YYYY-MM-DD HH:MM:SS] [LEVEL]  Message
Level Stream When
INFO stdout Normal operational messages
WARN stderr Non-fatal issues (e.g. TLS verification disabled, inline token in use, unexpected file type)
ERROR stderr Failures that cause an endpoint backup to be skipped
DEBUG stderr Detailed trace information (only with --verbose)

Note: Because DEBUG writes to stderr, it does not interfere with functions that capture stdout. This also means that when redirecting output to a log file you should include 2>&1 to capture all levels.

Example Output

[2026-04-03 02:00:01] [INFO]  === 3x-ui Database Backup ===
[2026-04-03 02:00:01] [INFO]  Config file         : /home/user/.config/3x-ui-db-backup/config.json
[2026-04-03 02:00:01] [INFO]  Destination folder  : /home/user/.local/share/3x-ui-db-backup
[2026-04-03 02:00:01] [INFO]  Endpoints to back up: 3
[2026-04-03 02:00:01] [INFO]  [vps-london] Starting backup.
[2026-04-03 02:00:02] [INFO]  [vps-london] Backup saved: /home/user/.local/share/3x-ui-db-backup/vps-london/vps-london_20260403_020001.db (245760 bytes)
[2026-04-03 02:00:02] [INFO]  [vps-london] Backup complete.
[2026-04-03 02:00:02] [INFO]  [vps-frankfurt] Starting backup.
[2026-04-03 02:00:02] [WARN]  TLS verification is DISABLED for https://vps2.example.com:54321
[2026-04-03 02:00:03] [INFO]  [vps-frankfurt] Backup saved: /home/user/.local/share/3x-ui-db-backup/vps-frankfurt/vps-frankfurt_20260403_020002.db (183296 bytes)
[2026-04-03 02:00:03] [INFO]  [vps-frankfurt] Rotating backups: removing 1 old backup(s) (keeping 14).
[2026-04-03 02:00:03] [INFO]  [vps-frankfurt] Backup complete.
[2026-04-03 02:00:03] [INFO]  [home-lab] Starting backup.
[2026-04-03 02:00:04] [WARN]  [home-lab] Using inline api_token from the main config file. Consider using 'token_file' instead.
[2026-04-03 02:00:04] [INFO]  [home-lab] Backup saved: /home/user/.local/share/3x-ui-db-backup/home-lab/home-lab_20260403_020003.db (98304 bytes)
[2026-04-03 02:00:04] [INFO]  [home-lab] Backup complete.
[2026-04-03 02:00:04] [INFO]  === Backup Summary ===
[2026-04-03 02:00:04] [INFO]  Successful : 3
[2026-04-03 02:00:04] [INFO]  Failed     : 0
[2026-04-03 02:00:04] [INFO]  All backups completed successfully.

Troubleshooting

Database download returns an unexpected HTTP status

If the API returns a non-200 status, the script logs an error and skips the endpoint. Common causes:

  • Wrong or expired API token — generate a new one from the 3x-ui web UI
  • The base_path in the config does not match the webBasePath configured in the panel, so the request is sent to the wrong URL
  • The panel is behind a reverse proxy that is intercepting the request

To verify the correct base_path, run x-ui settings on the panel server.

Script refuses to use the token file

The script enforces 600 permissions on token files. Check the current permissions and fix them:

stat ~/.config/3x-ui-db-backup/vps-london.token.json
chmod 600 ~/.config/3x-ui-db-backup/vps-london.token.json

Downloaded file is not a valid SQLite database

The script will log a [WARN] if the downloaded file does not appear to be a SQLite database. This typically means the API returned an error body (e.g. HTML or JSON) instead of the database. Run with --verbose and check the HTTP status codes. Common causes:

  • Wrong or expired API token
  • Wrong base_path
  • The panel is behind a reverse proxy that is intercepting the request

jq: command not found

Install jq using your distribution's package manager (see Requirements).

Backup directory is not created

The script creates the destination_folder (default: ~/.local/share/3x-ui-db-backup/) automatically on the first run. If creation fails, ensure your user has write permission to the parent directory, or create the directory manually:

mkdir -p ~/.local/share/3x-ui-db-backup
chmod 700 ~/.local/share/3x-ui-db-backup

If you set a custom destination_folder, ensure your user can write to that path.

Self-signed certificate errors

If the panel uses a self-signed TLS certificate, set "tls_verify": false for that endpoint in the configuration file. A [WARN] will be logged each time this setting is active.

No output from a cron job that failed

If you are using --cronmode and are not receiving failure emails from cron, verify that your system's cron daemon is configured to deliver mail. Check the MAILTO variable in the crontab, and confirm that a local mail transfer agent (MTA) such as postfix, nullmailer, or msmtp is installed and configured.


License

This project is licensed under the WTFPL — Do What The Fuck You Want To Public License.


Written by Paul Git and Claude AI