Automated cPanel backup
management

cPanelVault requests full backups of your cPanel accounts, downloads them over FTP with resume, and keeps a rotating local archive — with a live web UI, per-host scheduling and notifications.

Get started Read the docs View on GitHub
cPanelVault dashboard: one host finished successfully, another downloading its backup with progress, speed, ETA and live log

Everything you need, nothing you don't

A focused tool that does one thing well: get full cPanel backups off the server and keep them safe on storage you control.

Live dashboard

See every host at a glance: current phase, download progress with speed and ETA, the live log of the run, next scheduled run and one-click manual backups.

Backups page

Browse the local archives of each host with size, download date, expiry and the run that will remove them, plus free space on the backup volume.

Stability check

Polls the FTP until the archive stops growing across several checks before downloading — no partial files, no placeholders.

Resilient by design

Resumable downloads, socket timeouts, a free-space check before each download, and automatic retries for backup requests and remote deletes.

Safe concurrency

A per-account lock stops the scheduler, the web UI and the CLI from backing up the same cPanel account (same host name and username) twice at the same time.

Retention management

Archives older than a per-host number of days are removed once each run has downloaded its new archive, so storage never grows unbounded.

Multi-host

Back up any number of cPanel accounts from one instance, each with its own cron schedule, retention and credentials.

Notifications

Results with the run log via Telegram, SMTP or Resend — enable one channel or all of them.

Official Docker images

Multi-arch images (amd64, arm64) on Docker Hub and GHCR, with a healthcheck and volumes for backups and state.

See what's happening, while it happens

Every run is visible from the browser: no need to tail container logs to know where a backup is.

Dashboard with three hosts: two successful, one downloading with the live log expanded

Dashboard. Status, last archive, duration, schedule and next run for each host. A running backup shows its phase (checking FTP, waiting for cPanel, stability check, downloading, retention), progress with speed and ETA, and the live log.

Backups page listing local archives per host with size, download date, expiry and removal date

Backups page. Every archive on the backup volume, grouped by host: size, cPanel timestamp, download date, expiry and the scheduled run that will remove it. Expired and soon-to-expire archives are highlighted. (Scroll inside the image.)

Dashboard on a phone

Mobile. The web UI adapts to small screens, so you can check a run from your phone.

The full backup lifecycle, unattended

At the scheduled time (or when you click Run now) cPanelVault runs these steps for each host.

1

Check the FTP server

If a backup from a previous session is still on the server, it's downloaded and removed first, so nothing is lost or left behind.

2

Request a full backup

cPanelVault calls the cPanel UAPI (fullbackup_to_homedir). On connectivity errors the request is retried after 10 and 30 minutes.

3

Wait for the archive to be ready

It polls the FTP and waits until the file size is identical across several consecutive checks — proof that cPanel has finished writing.

4

Download & clean up the server

After checking free disk space, the archive is downloaded with automatic resume, then deleted from the FTP server. The remote copy is removed only after the local download succeeds.

5

Apply retention & notify

Local archives older than the host's retention period are deleted, the status is saved for the dashboard, and a notification with the run log is sent.

Up and running in minutes

All you need is Docker with Compose. Official images are published for every release:

tags: latest · X.Y.Z · dev

Docker Compose

# Get the compose file and a sample config
mkdir cpanelvault && cd cpanelvault
RAW=https://raw.githubusercontent.com/gioxx/cPanelVault/main
curl -LO $RAW/docker-compose.yml
curl -Lo ftp_config.json $RAW/ftp_config_sample.json

# Add your hosts and credentials
nano ftp_config.json

# Start
docker compose up -d

The web UI is available at http://localhost:8080. Set TZ in docker-compose.yml to your timezone: cron schedules use it.

Store backups on a specific disk

# docker-compose.yml
volumes:
  - ./ftp_config.json:/app/ftp_config.json:ro
  - "/mnt/my-disk/cPanel Backups:/backups"
  - data:/data

Host path on the left, container path on the right. Quote the entry if the path contains spaces, and keep "backup_local_dest_folder": "/backups" in the config.

Update

docker compose pull && docker compose up -d

Pin a release (e.g. gfsolone/cpanelvault:2.3.1) instead of latest to upgrade on your own schedule.

Configure, run, integrate

Everything below is also in the README (italiano).

Configuration

All settings live in one JSON file, ftp_config.json (mounted read-only at /app/ftp_config.json in Docker). Each top-level key other than notifications is a host; the key (cpanel1, website, …) is the name used in the web UI, the CLI and the API.

{
  "notifications": { ... },
  "cpanel1": {
    "host": "ftp.example.com",
    "ftp_username": "backup@example.com",
    "ftp_password": "your-ftp-password",
    "cpanel_username": "exampleuser",
    "cpanel_api_token": "YOUR_TOKEN",
    "backup_local_dest_folder": "/backups",
    "mail_to_notify": "you@example.com",
    "time_to_wait": 60,
    "retention_days": 30,
    "schedule": "0 2 * * *"
  }
}

Archives are stored as <backup_local_dest_folder>/<domain>/backup-*.tar.gz, where the domain is host without the ftp. prefix.

Host fields

FieldDefaultDescription
host—FTP hostname, with or without the ftp. prefix. The cPanel API is called on the same name without the prefix (port 2083).
ftp_username—FTP user that can read the account's home directory.
ftp_password—FTP password.
cpanel_username—cPanel account username.
cpanel_api_token—cPanel API token (see below).
backup_local_dest_folder—Local root folder for archives: /backups in Docker.
mail_to_notify—Address cPanel emails when the backup is ready.
time_to_wait60Seconds between size-stability checks while cPanel writes the archive.
retention_days30How many days local archives are kept, counted from their download.
schedule—Cron expression (5 fields) in the TZ timezone. Numeric weekdays are supported (0 and 7 = Sunday). Omit for manual-only hosts.
request_after_downloadtrueWhen a leftover backup is found on the FTP server and downloaded, request a fresh one right after, so the run still produces a current archive.

Generating a cPanel API token

  1. Log in to cPanel and open Manage API Tokens.
  2. Create a token with a descriptive name (e.g. cpanelvault).
  3. Paste its value into cpanel_api_token.

Environment variables

VariableDefaultDescription
TZUTCTimezone for cron schedules and for the times shown in the web UI.
CONFIG_FILE/app/ftp_config.jsonConfig file used by the web UI and scheduler. The CLI uses --config instead.
STATUS_FILE/data/status.jsonLast-run status of every host, read by the dashboard.
LOCK_DIRlocks/ next to STATUS_FILELock files that prevent concurrent backups of the same account.
QUIET_ACCESS_LOGtrueHides successful dashboard refreshes, /api/status and static requests from the access log. Set to false to log everything.

Defaults shown are the ones set in the Docker image; outside Docker, CONFIG_FILE and STATUS_FILE default to ftp_config.json and status.json in the working directory.

Notifications

Channels are configured in the notifications key of ftp_config.json. Every enabled channel receives the result of each run, including its log.

Telegram

Create a bot with @BotFather. For chat_id use your user ID (@userinfobot) or a channel/group ID prefixed with -100.

"telegram": {
  "enabled": true,
  "bot_token": "123456789:AABBcc...",
  "chat_id": "-100123456789"
}

SMTP

Any SMTP server. Use port: 587 with use_ssl: false for STARTTLS, or port: 465 with use_ssl: true for implicit TLS. For Gmail, use an App Password.

"smtp": {
  "enabled": true,
  "host": "smtp.gmail.com",
  "port": 587,
  "use_ssl": false,
  "username": "you@gmail.com",
  "password": "app-password",
  "from": "cPanelVault <you@gmail.com>",
  "to": "recipient@example.com"
}

Resend

Sign up at resend.com, verify your sender domain and create an API key.

"resend": {
  "enabled": true,
  "api_key": "re_xxxx...",
  "from": "cPanelVault <backup@yourdomain.com>",
  "to": "recipient@example.com"
}

Portainer

Create the config on the Docker host first (e.g. /opt/cpanelvault/ftp_config.json), then add a stack (Stacks → Add stack → Web editor):

services:
  cpanelvault:
    image: gfsolone/cpanelvault:latest
    ports:
      - "8080:8080"
    volumes:
      - /opt/cpanelvault/ftp_config.json:/app/ftp_config.json:ro
      - cpanelvault_backups:/backups
      - cpanelvault_data:/data
    environment:
      TZ: Europe/Rome
    restart: unless-stopped

volumes:
  cpanelvault_backups:
  cpanelvault_data:

To upgrade: Stacks → cpanelvault → Update the stack with Re-pull image enabled.

CLI

The same image (or a local pip install -r requirements.txt) provides a command-line interface. Inside the container, prefix commands with docker compose exec cpanelvault so they share the lock and status with the web UI.

# Back up one host, or all of them sequentially
python main.py backup cpanel1
python main.py backup --all

# Remove expired archives (preview first)
python main.py clean cpanel1 --dry-run
python main.py clean --all

# Web UI + scheduler (what the container runs)
python main.py serve --port 8080

# Use a different config file
python main.py --config /path/to/config.json backup --all

backup exits with code 1 if any host fails or is skipped, so it can be used from cron or scripts.

REST API

MethodPathDescription
GET/Dashboard (HTML)
GET/backupsBackups page (HTML)
GET/api/statusLast-run status of every host (JSON), also used by the healthcheck
GET/api/hostsConfigured hosts (JSON)
GET/api/backupsLocal archives, retention and volume usage (JSON)
POST/backup/<name>Start a backup of one host in the background
curl -X POST http://localhost:8080/backup/cpanel1
curl http://localhost:8080/api/status
The web UI and API have no built-in authentication. Keep port 8080 on a trusted network, or put it behind a reverse proxy with authentication.

Behaviour & FAQ

When are local archives deleted?

An archive expires when it's older than the host's retention_days, counted from its download (file modification time). Expired archives are removed by the cleanup step at the end of the next backup of that host, which runs only after the new archive has been downloaded: a run that fails earlier (backup request, wait or download) deletes nothing. The Backups page shows the run that will remove each archive; python main.py clean <host> removes them immediately.

Can two backups of the same account overlap?

Not when they use the same host name. Only one backup per lock key runs at a time, whether it's started by the scheduler, the Run now button or the CLI. The lock is keyed on the FTP host (case, ftp. prefix and trailing dot ignored) plus cpanel_username, so config entries with the same host and username are serialized too: the entry actually running shows Running…, the others show Busy, and a second run is skipped with a warning.

cPanelVault can't tell that two different host names lead to the same account (e.g. the customer domain and the provider's server hostname): such entries get separate locks and could run at the same time, racing on the same remote archive. Use the same host value for every entry of one account.

What if the container is stopped mid-backup?

At the next start, a backup left in the "running" state is marked as interrupted. Partially downloaded archives are resumed by the next run.

What happens on network problems?

The cPanel backup request is retried after 10 and 30 minutes on connectivity errors; FTP downloads retry every 10 seconds and resume where they stopped; remote deletes are retried up to 5 times. If the backup volume doesn't have enough free space, the download is aborted up front with a clear error.

Where are the logs?

On stdout (docker compose logs -f), in the dashboard's live log for each run, and in notifications. Routine dashboard refreshes and healthchecks are kept out of the access log unless QUIET_ACCESS_LOG=false.