EST. 2026 — Notes from real server work SYSADMIN · SECURITY · AI · DIRECTADMIN Issue 01
THE SYSADMIN ALMANAC
Home / DirectAdmin / Set DirectAdmin Admin Backup Cron from the CLI Without the Panel
DirectAdmin

Set DirectAdmin Admin Backup Cron from the CLI Without the Panel

by Ton-Or ·24 Jul 2026·8 min read
Set DirectAdmin Admin Backup Cron from the CLI Without the Panel

Click Submit once on CMD_ADMIN_BACKUP, and behind the scenes DirectAdmin just appends one line to a plain text file — once I understood that line's format, I could set up backup cron jobs for ten users at a time from the shell without clicking through each one.

I needed to set up Admin Backup for several dozen customer users on a DirectAdmin server. Clicking through CMD_ADMIN_BACKUP for each one would have taken all day, so I looked into what it writes and where when I click Submit. It turned out that the Submit button does something much simpler than I expected — it just appends one line to a plain text file, and a daemon checks that file repeatedly to run the jobs on schedule. Once I understood the line's format, setting up a cron job from the shell was just a matter of using echo to append to the file.

All users, IPs, and FTP passwords in this article are examples (the host is ftp.example.com). Real values must never go into the article. The line structure and mechanism are real.

The file where DA actually stores cron jobs is backup_crons.list

All Admin Backup cron jobs go into a single file, rather than root's crontab as many people assume:

ls -l /usr/local/directadmin/data/admin/backup_crons.list
# -rw------- 1 diradmin diradmin  842 ... backup_crons.list

There are three things to remember — the file owner must be diradmin:diradmin, permissions must be 600 (it contains FTP passwords, so nobody else should be able to read it), and the daemon that processes this file is called dataskq. It does not act immediately; it checks roughly every 60 seconds. If I edit the file and its ownership or permissions end up wrong, dataskq will not touch it, so I have to restore the owner/mode after every edit.

The actual path of a cron jobbackup_crons.listI write this line->dataskqChecks every ~60 seconds->task.queueThe actual job queue->FTP /MondayBackup files at the destination

Each line's format: id=URL-encoded parameters

One line represents one cron job, in the format <id>=<พารามิเตอร์แบบ URL-encode>, where <id> is a unique integer (1, 2, 3, ...). To add a new one, find the highest existing number and add +1.

# /usr/local/directadmin/data/admin/backup_crons.list
1=action=backup&hour=5&minute=0&dayofweek=%2A&where=local&...&select0=webuser1
2=action=backup&hour=3&minute=0&dayofweek=0&where=ftp&...&select0=webuser2&select1=webuser3

There is a set of required parameters — always action=backup, type=admin (an admin-level cron job), value=multiple (even when selecting just one user, it must be multiple), owner=admin, then the schedule fields hour minute dayofmonth dayofweek month, and where to specify storage on ftp or local. Specify the users to back up as select0, select1, select2, continuing the index in sequence. Do not reuse an index, because the values will overwrite each other.

There is a trap here that confused me for a long time. The line always includes when=now — do not mistake it for an instruction to run immediately. It is just a value DA writes along with the rest; hour/minute/dayofweek determine the actual schedule.

when=now

Misleading, not the schedule

Always included in every line, regardless of when the job is scheduled. Just skip over it; there is no need to change it.

hour / minute / dayofweek

What actually sets the run time

These are the fields that actually matter — dayofweek 0 means Sunday, and %2A means every day. For every day at 3 a.m., use hour=3 minute=0 dayofweek=%2A.

For URL encoding, there is no need to encode every character the way the panel does (the panel goes as far as converting digits to %30). DA's parser accepts both forms. Only three characters need encoding because they conflict with the key=value&key=value syntax: write * as %2A, / as %2F, and = as %3D. Everything else can be left as plain text for readability; the panel will re-encode it the next time it saves.

FTP target: append_to_path and base64 passwords

For FTP storage, the full ftp_* block must be present, otherwise DA will silently do nothing or fall back to local storage. The two common mistakes involve the password and destination path.

The FTP password is stored in the file as base64, not plaintext or a hash — it can be decoded, so anyone who can read the file can see the password (which is why the mode must be 600). Generate the token with printf, not echo, because echo appends a newline that makes the base64 value wrong:

printf '%s' 'MockFtpPass_2026' | base64
# TW9ja0Z0cFBhc3NfMjAyNg==

The append_to_path option is useful and worth using. Normally, if daily backups go to the same path, the new day's files overwrite the previous day's files. With append_to_path=dayofweek, DA appends the day name automatically, giving me a 7-day backup rotation without having to write the logic myself.

How append_to_path=dayofweek worksftp_path /admin->+ Automatic day name->/admin/Monday ... /admin/Sunday7-day rotation, separate daily files

Other values accepted by append_to_path are dayofmonth (appends /01../31), date (/YYYY-MM-DD), datetime (/YYYY-MM-DD-HHMM), or nothing to append nothing. Remember that ftp_path is relative to the FTP user's chroot home, not the actual path on disk.

Add cron jobs from the shell with a short script

Putting everything into one script, it takes hour, minute, and dayofweek, followed by any number of users to back up. The core is finding the next id, building the line, and appending it to the file, then restoring the owner/mode so dataskq accepts it:

#!/bin/bash
# da-add-backup-cron.sh <hour> <minute> <dayofweek> <user1> [user2] ...
set -euo pipefail
CRONS=/usr/local/directadmin/data/admin/backup_crons.list
HOUR="$1"; shift
MIN="$1";  shift
DOW="$1";  shift          # 0-7 หรือ %2A (ทุกวัน)
USERS=("$@")

# หาเลข id ถัดไป
NEXT=$(awk -F= 'NF>0{print $1}' "$CRONS" | sort -n | tail -1)
NEXT=$((NEXT + 1))

# ประกอบ selectN ให้แต่ละ user
SEL=""; i=0
for u in "${USERS[@]}"; do SEL+="&select${i}=${u}"; i=$((i+1)); done

# รหัส FTP ต้องเป็น base64 (ตัวอย่าง mock)
FTP_PW=$(printf '%s' 'MockFtpPass_2026' | base64)

LINE="${NEXT}=action=backup&hour=${HOUR}&minute=${MIN}&dayofmonth=%2A&dayofweek=${DOW}&month=%2A"
LINE+="&where=ftp&ftp_ip=ftp.example.com&ftp_port=21&ftp_username=backupuser&ftp_password=${FTP_PW}"
LINE+="&ftp_path=%2Fadmin&ftp_secure=no&append_to_path=dayofweek"
LINE+="&database_data_aware=yes&email_data_aware=yes&trash_aware=yes"
LINE+="&type=admin&value=multiple&owner=admin&when=now${SEL}"

cp "$CRONS" "${CRONS}.bak-$(date +%Y%m%d-%H%M)"   # สำรองก่อนแตะเสมอ
echo "$LINE" >> "$CRONS"
chown diradmin:diradmin "$CRONS"
chmod 600 "$CRONS"
echo "เพิ่ม cron id=${NEXT}: ${HOUR}:${MIN} dow=${DOW} users=${USERS[*]}"

Usage:

# ทุกอาทิตย์ตีสาม backup 2 user
./da-add-backup-cron.sh 3 0 0 webuser1 webuser2

# ทุกวันตีสอง user เดียว (dayofweek = %2A)
./da-add-backup-cron.sh 2 0 '%2A' webuser3

When setting this up for several dozen users, I recommend staggering the times by about 5 minutes per user. Do not schedule them all for exactly 2 a.m., because backing up the whole server at once will spike I/O and RAM usage, slowing down any websites in use at the time. A loop using i*5 minutes is enough.

Test immediately without waiting for the scheduled time

To avoid waiting until 3 a.m. to see whether the setup is correct, there is a shortcut: write a job directly to task.queue, which dataskq picks up on its next pass (~60 seconds), independently of the schedule:

TASK="action=backup&type=admin&value=multiple&owner=admin&when=now"
TASK+="&where=ftp&ftp_ip=ftp.example.com&ftp_port=21&ftp_username=backupuser"
TASK+="&ftp_password=$(printf '%s' 'MockFtpPass_2026' | base64)"
TASK+="&ftp_path=/admin&ftp_secure=no&append_to_path=dayofweek"
TASK+="&database_data_aware=yes&email_data_aware=yes&trash_aware=yes&select0=webuser1"
echo "$TASK" >> /usr/local/directadmin/data/task.queue

This is cleaner than changing the cron job to run the next minute and then changing it back, because it leaves the actual cron line untouched. Wait a moment, then check the log to see whether it went through.

Check that it went through

After adding a line or submitting a job to task.queue, I check three places — first, open CMD_ADMIN_BACKUP and look for the new entry under Current Backup Crons (if it does not appear, the file's owner/mode is wrong and dataskq cannot read it). Second, check the log to confirm that a backup was actually triggered. Third, look for the destination files on FTP in the folder named after the day:

tail -f /var/log/directadmin/admin_backups.log
journalctl -u directadmin --since "5 min ago" | grep action=backup

Traps I often run into

The panel shows the cron job, but it does not run

Almost every time, backup_crons.list is not owned by diradmin:diradmin or its mode is not 600. After editing the file, restore both with chown/chmod, then check systemctl is-active directadmin to see whether dataskq is still running.

The FTP password in the file is base64, not encryption

base64 can be decoded immediately, so anyone who can read this file effectively has the FTP password. This is why the mode must be exactly 600, and when writing an article or sharing config, never expose the real token. Always mask it or use a mock value — in this article, the host, user, and password are all made up.

Two more things I have run into — if the password does not work even though it was typed correctly, check whether echo was accidentally used instead of printf '%s' during base64 encoding, because echo appends a newline and leaves the token off by one character. And if where=ftp is set but the ftp_* block is incomplete, DA will not report a clear error; it will silently use local storage or do nothing, leaving me hunting for the cause for ages.

Edit or delete an entry

Delete an individual entry with sed -i '/^2=/d' backup_crons.list (deletes id=2). Editing means deleting and appending a new entry. Always make a backup with cp before touching the file, then restore ownership and permissions with chown/chmod afterward.

Once I understood that the panel's Submit button simply appends one line to this file, setting up backups for dozens of users went from taking an entire afternoon to a single shell loop. I use this structure to set up cron jobs for the whole server in one pass, then leave dataskq to do its work. Feel free to adapt the time slots and destinations to suit your own server. If I have left anything out or made a typo, I will come back and correct it later.

Want us to set this up?

Get help with server setup, configuration, migration, or troubleshooting. Save time for the work that matters.

Get a quote

Hit something like this? Stuck somewhere?

Come trade notes on the LinuxThai board

→ Continue on the board

Comments 0

No comments yet — be the first