- Shell 100%
- 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> |
||
|---|---|---|
| .gitignore | ||
| 3x-ui-db-backup.sh | ||
| AGENTS.md | ||
| example.config.json | ||
| example.token.json | ||
| LICENSE | ||
| README.md | ||
3x-ui Database Backup Script
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
- Requirements
- Installation
- Configuration
- API Token Management
- Usage
- Backup File Structure
- Retention / Rotation
- Scheduling with Cron
- Logging
- Troubleshooting
- License
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: Bearerheader; 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
webBasePathsupport — 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.
-
Ensure
~/.local/binis on yourPATH(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.
-
Create the install directory and download the script (the
.shextension 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.shOr 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 -
Make the script executable:
chmod +x ~/.local/bin/3x-ui-db-backup -
Create the configuration directory:
mkdir -p ~/.config/3x-ui-db-backup chmod 700 ~/.config/3x-ui-db-backup -
Create your main configuration file (see Configuration below):
nano ~/.config/3x-ui-db-backup/config.json -
Create a token file for each endpoint (see API Token Management below):
nano ~/.config/3x-ui-db-backup/vps-london.token.json -
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 -
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
600at 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 |
Inline Token (Not Recommended)
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
Recommended: Cron Mode (no log file required)
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
DEBUGwrites to stderr, it does not interfere with functions that capture stdout. This also means that when redirecting output to a log file you should include2>&1to 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_pathin the config does not match thewebBasePathconfigured 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