ERP Snapshot: setup guide for a cPanel account

How to add automatic change tracking to a cPanel account: every code change is committed under the FTP account that made it, pushed to GitHub and emailed every night. For whoever sets it up; it needs root (WHM) access once.

Overview#

How to add automatic change tracking to a cPanel account: every code change is committed under the FTP account that made it, pushed to GitHub and emailed every night. For whoever sets it up; it needs root (WHM) access once.

Important
Before you start: you need root (WHM) access, a new private and empty GitHub repository, and the account's username and main domain. Never paste the email password from .smtp into a document or a chat.

1 Part A: Before you start#

This part explains what the setup does and what you need to have ready.

1.1 How it works#

Code changes are committed every 5 minutes under the FTP account that made them, pushed to GitHub and emailed every night at 23:30.

PartRuns asFileWhen
Snapshotthe account~/erp-snapshot/snapshot.shevery 5 minutes
Nightly reportthe account~/erp-snapshot/daily_report.sh23:30
FTP log copyroot/usr/local/sbin/erp-ftplog-account.shevery minute

Sections marked (manual) are done in cPanel File Manager or on GitHub. Terminal commands appear only where git, cron or a check needs them: account terminal is the account's own cPanel Terminal, root terminal is WHM → Terminal.

1.2 What you need#

Replace these placeholders everywhere in this guide:

PlaceholderMeaningExample
USERNAMEthe cPanel usernameerpnasirgroup
DOMAINthe account's main domainerp.nasirgroup.org
OWNER/REPOthe GitHub repositoryerpcombd/erpnasirgroup
  • Root (WHM) access, for the root copy job.
  • A new private, empty GitHub repository, with no README.
  • The account's username and main domain. To look them up in the root terminal:
Code
grep 'DOMAIN' /etc/userdomains
  • The folder to watch: the app folder, or public_html for everything.

2 Part B: Prepare the files (manual)#

The scripts are copied from a working account and adjusted by hand in cPanel File Manager.

2.1 Copy the files#

Six files from an account where the snapshot already works go into a new erp-snapshot folder.

  1. In the File Manager of a working account, open Settings, tick Show Hidden Files, open erp-snapshot and download these six files:config.sh
    snapshot.sh
    daily_report.sh
    mask-secrets.sed
    exclude.txt
    .smtp (the report email login)
  2. In the new account's File Manager, create a folder erp-snapshot in the home folder /home/USERNAME and upload the six files into it.
  3. Set permissions (right-click → Change Permissions):config.sh, snapshot.sh, daily_report.sh: 711
    .smtp: 600
Important
Warning: never copy repo.git, repo-wrong.git, ftp-uploads.log, snapshot.log, baseline.out, reports or before-ftp-names. They belong to the account they came from.
Note
Note: the downloaded .smtp holds the email password. Delete it from your computer once it is uploaded.

2.2 Edit config.sh#

Five lines change. Open config.sh with Edit in File Manager and set them to:

SET THESE LINES
Code
APP_DIR="/home/USERNAME/public_html"
BASE_DIR="/home/USERNAME/erp-snapshot"
GITHUB_REMOTE="git@github-erp:OWNER/REPO.git"
GITHUB_WEB="https://github.com/OWNER/REPO"
REPORT_LABEL="Name shown in the email subject"
LineRule
APP_DIRNo / at the end: a trailing slash makes every change show as main login. Never the whole home folder /home/USERNAME.
BASE_DIRAlways /home/USERNAME/erp-snapshot.
GITHUB_REMOTEMust start with git@github-erp:. With git@github.com: every push is refused.
GITHUB_WEBNo .git at the end, or the link in the email breaks.
REPORT_LABELAdd the line at the end of the file if it is missing.

Leave the other lines as they are, including REPORT_TO, FTP_LOG and NO_FTP_NAME. Accounts on the same server share the report address, so REPORT_TO stays.

2.3 Check daily_report.sh and exclude.txt#

daily_report.sh. Open it and confirm this line, around line 82, reads exactly as below. It is what puts the label in the email subject.

EXPECTED LINE
Code
SUBJECT="${REPORT_LABEL:+[$REPORT_LABEL] }ERP changes $TODAY: $FILES file(s)"

exclude.txt. The section under # CREDENTIALS lists files that are never recorded. Add the names of this account's files that hold passwords, if they are not covered; whether to exclude them is your decision. Files not listed are still recorded, with recognisable passwords replaced by ***MASKED***.

3 Part C: Git and GitHub (account terminal)#

This part builds the account's change history, checks it, and connects it to GitHub.

3.1 Build the history#

The first commit records every tracked file once, with secret masking on. It runs in the background, so a closed browser tab cannot stop it.

First confirm you are in the right account and that nothing is running or built yet:

Terminal
whoami
cd ~/erp-snapshot
ps -u $(whoami) -o args | grep '[g]it '
ls -d repo.git 2>/dev/null

whoami must print USERNAME. The two lines after cd must print nothing. Then start the build:

Terminal
setsid nohup bash -c 'source ./config.sh &&
  g init -q && g symbolic-ref HEAD refs/heads/main && g config core.fileMode false &&
  cp exclude.txt repo.git/info/exclude &&
  g config filter.mask.clean "sed -E -f $BASE_DIR/mask-secrets.sed" &&
  g config filter.mask.smudge cat && g config filter.mask.required true &&
  printf "*.php filter=mask\n*.js filter=mask\n*.json filter=mask\n" > repo.git/info/attributes &&
  g add -A && g commit -q -m "baseline: production state" && echo DONE' > baseline.out 2>&1 < /dev/null &

Check progress at any time, even after reopening the terminal:

Code
cat ~/erp-snapshot/baseline.out
du -sh ~/erp-snapshot/repo.git

Wait until baseline.out says DONE. A normal app takes a few minutes; a large public_html (about 190,000 files) took more than 25 minutes.

Important
Warning: never start the build a second time while one is running.

3.2 Check the history#

Nothing goes to GitHub until this check passes, because a pushed history cannot be taken back.

Terminal
cd ~/erp-snapshot && source config.sh
g --no-pager log --oneline -1
g ls-files | wc -l
g ls-files | cut -d/ -f1 | sort | uniq -c | sort -rn | head -12
g grep -l -F '***MASKED***' HEAD | wc -l
CommandMust show
logbaseline: production state
ls-files | wc -lthe number of tracked files
folder listonly the folders of what you meant to watch. Names such as mail, logs, erp-snapshot or other domains mean APP_DIR is wrong.
MASKED counta number above 0: masking ran

If any line is wrong: rename repo.git to repo-wrong.git in File Manager, fix config.sh, and build the history again. Delete repo-wrong.git once the new history passes.

3.3 Connect GitHub#

Each repository gets its own deploy key, created in the account it belongs to; GitHub accepts a key on one repository only.

Create the key:

Code
whoami
ls ~/.ssh/erp_github 2>/dev/null && echo "KEY ALREADY EXISTS - stop"
ssh-keygen -t ed25519 -f ~/.ssh/erp_github -N "" -C erp-snapshot-USERNAME
cat ~/.ssh/erp_github.pub

On GitHub (manual): open the repository → Settings → Deploy keys → Add deploy key. Paste the line printed by cat, tick Allow write access, and save. Without write access the push is refused.

Add the github-erp nickname and test it:

Terminal
grep -q '^Host github-erp$' ~/.ssh/config 2>/dev/null || cat >> ~/.ssh/config <<'EOF'
Host github-erp
  HostName ssh.github.com
  Port 443
  User git
  IdentityFile ~/.ssh/erp_github
  IdentitiesOnly yes
EOF
chmod 600 ~/.ssh/config
ssh -T git@github-erp

Type yes if asked to continue connecting. It must answer Hi OWNER/REPO! You've successfully authenticated.

Note
Note: port 443 is used because hosting firewalls often block the usual SSH port.

3.4 First push#

The history goes to GitHub once now; after this the nightly report pushes it automatically.

Terminal
cd ~/erp-snapshot && source config.sh
g remote add origin "$GITHUB_REMOTE" 2>/dev/null || g remote set-url origin "$GITHUB_REMOTE"
g remote -v
g push -u origin main
g status -sb | head -1
  • g remote -v must show git@github-erp:OWNER/REPO.git twice. Stop if it shows github.com, and fix GITHUB_REMOTE in config.sh.
  • A large history takes a few minutes to upload. The push ends with main -> main.
  • The last line must read ## main...origin/main with nothing after it.

4 Part D: Scheduled jobs#

Two cron jobs run inside the account, and one root job per account feeds them the FTP log.

4.1 The account's cron jobs#

Two jobs run in the account: the snapshot every 5 minutes and the report at 23:30. Run these in the account terminal:

Terminal
crontab -l 2>/dev/null | grep erp-snapshot
(crontab -l 2>/dev/null
 echo "*/5 * * * * $HOME/erp-snapshot/snapshot.sh >/dev/null 2>&1"
 echo "30 23 * * * $HOME/erp-snapshot/daily_report.sh >/dev/null 2>&1") | crontab -
crontab -l | grep erp-snapshot
  • The first line must print nothing. If it prints lines, the jobs exist already: skip the three lines that add them.
  • The last line shows both jobs with /home/USERNAME/erp-snapshot/.... They also appear on the account's Cron Jobs page in cPanel.
Important
Warning: add the jobs either here or on the Cron Jobs page, never both. Added twice, they run twice and send two emails a night.

4.2 Root's copy job#

The FTP logs are readable only by root, so one root job per account copies that account's upload and deletion lines into ~/erp-snapshot/ftp-uploads.log every minute. Open WHM → Terminal; whoami must print root.

Once per server, check the shared script exists:

Code
whoami
ls -la /usr/local/sbin/erp-ftplog-account.sh

If it says "No such file", install it:

ONCE PER SERVER
Terminal
cat > /usr/local/sbin/erp-ftplog-account.sh <<'EOF'
#!/bin/bash
# Pass one cPanel account a running copy of its own FTP log lines.
# Usage: erp-ftplog-account.sh <cpanel user> <main domain of that account>
U="$1"; DOM="$2"; D=/usr/local/apache/domlogs
[ -n "$U" ] && [ -n "$DOM" ] || exit 1
{ cat "$D/ftp.$DOM-ftp_log" "$D/ftpxferlog" 2>/dev/null
  tail -n 20000 /var/log/messages 2>/dev/null | grep -a 'pure-ftpd\[.*\[NOTICE\] Deleted '
} | grep -a " /home/$U/" |
  su -s /bin/bash "$U" -c 'cd ~/erp-snapshot && touch ftp-uploads.log &&
    cat ftp-uploads.log - | awk "!seen[\$0]++" | tail -n 20000 > ftp-uploads.tmp &&
    mv ftp-uploads.tmp ftp-uploads.log'
EOF
chown root:root /usr/local/sbin/erp-ftplog-account.sh
chmod 700 /usr/local/sbin/erp-ftplog-account.sh
FOR EACH ACCOUNT
Terminal
U=USERNAME
DOM=DOMAIN
echo "* * * * * root /usr/local/sbin/erp-ftplog-account.sh $U $DOM >/dev/null 2>&1" \
  > /etc/cron.d/erp-ftplog-$U
chmod 644 /etc/cron.d/erp-ftplog-$U
cat /etc/cron.d/erp-ftplog-$U
/usr/local/sbin/erp-ftplog-account.sh $U $DOM
ls -la /home/$U/erp-snapshot/ftp-uploads.log
  • The cat line must show the real username and domain, not $U.
  • The last line must list the file, owned by the account. It may be empty until someone uses FTP there.
  • DOMAIN is the part between ftp. and -ftp_log in the account's log name. This lists them:
Code
ls /usr/local/apache/domlogs/ | grep ftp
Note
Note: the mhafuzdev account uses an older single-account script, /etc/cron.d/erp-ftplog. Leave it as it is.

5 Part E: Finish and maintain#

This part confirms the setup works and covers developer access, problems and removal.

5.1 Check everything#

A setup is finished when the test report arrives without [PUSH FAILED] and the scheduler makes a snapshot run on its own.

Test report (account terminal). This sends a real email and pushes to GitHub:

Terminal
cd ~/erp-snapshot && bash daily_report.sh; tail -2 snapshot.log

The log must end with report sent: [Label] ERP changes ... without [PUSH FAILED]. That night's 23:30 email then covers only changes made after this run.

All accounts at once (root terminal). Add the new username to the list:

Terminal
for u in mhafuzdev pandughar erpnasirgroup USERNAME; do
  h=/home/$u/erp-snapshot
  echo; echo "=== $u ==="
  crontab -l -u $u 2>/dev/null | grep erp-snapshot
  echo "log copy refreshed: $(stat -c %y $h/ftp-uploads.log 2>/dev/null | cut -d. -f1)"
  echo "last snapshot run:  $(cat $h/repo.git/last-snapshot 2>/dev/null)"
  echo "last log line:      $(tail -1 $h/snapshot.log 2>/dev/null)"
done
echo; echo "time now: $(date '+%F %T')"
LineHealthy
cron linesexactly two per account: snapshot.sh and daily_report.sh
log copy refreshedwithin the last minute of time now
last snapshot runwithin the last 5 minutes (hourly for mhafuzdev); 20261026071502 means 07:15:02 on 26 October
last log linethe latest snapshot committed or report sent
Note
Note: run it once more about 6 minutes after setup without running anything by hand. A newer last snapshot run proves the scheduler works.

5.2 Developer FTP accounts#

Names appear only for changes made through a personal FTP account; until then every change is labelled main login.

  1. In the account's cPanel → FTP Accounts, create one account per developer. Set Directory to the folder in APP_DIR, written from the home folder (for example public_html). Clear the suggested value first.
  2. Developers connect with any FTP program: protocol FTP with explicit TLS, server ftp.DOMAIN, port 21, and the full login including the domain (for example rahim@DOMAIN).
  3. Change the cPanel password and keep it private. Anyone who has it can still edit as main login.
Important
Warning: never tick "Delete the User's Home Directory" when deleting one of these FTP accounts. Its directory is the live site.

5.3 Troubleshooting#

Most of these came up while setting up the first accounts; each has a one-line fix.

SymptomCauseFix
Permission denied (publickey) on push; email subject [PUSH FAILED]GITHUB_REMOTE uses github.comfix it in config.sh, then run g remote set-url origin "$GITHUB_REMOTE" after source config.sh
ssh -T greets a different repositorythe key was added to the wrong repositorydelete it there, add it to OWNER/REPO
Every change labelled main loginAPP_DIR ends in /, or no developer FTP accounts yetremove the /; create the developer FTP accounts
Building the history runs for a very long timeAPP_DIR is the whole home folderkill the git processes, rename repo.git, fix APP_DIR, build the history again
Two git add processes in psthe build was started twicekill both, rename repo.git, start once
Two emails a night from one accountcron jobs added twiceas root, remove the duplicate lines with the command below
su: command not foundyou are inside a cPanel accountuse WHM → Terminal for root steps
cd: /home/OTHER/...: No such filean account cannot see another account's foldersrun that step as root
last snapshot run never changesno cron jobs, or scripts lack permission 711add the account's cron jobs; set the permissions from Copy the files
log copy refreshed is oldno root job for the accountset up root's copy job
FTP log not available, no names in snapshot.logthe log copy stopped refreshingas root, run root's copy script by hand and read its error
REMOVE DUPLICATE CRON LINES (ROOT)
Terminal
crontab -l -u USERNAME | awk '!seen[$0]++' | crontab -u USERNAME -

5.4 Removing a setup#

Four places hold a setup; clear all four, the deploy key first.

  1. GitHub (manual): delete the account's deploy key in the repository's Settings → Deploy keys. Until it is deleted, the key file on the server can push.
  2. Account cron jobs (manual): delete the two erp-snapshot lines on the account's Cron Jobs page.
  3. Root job (root terminal): remove the account's schedule file, never /etc/cron.d/erp-ftplog, which belongs to mhafuzdev.
  4. Files (manual, hidden files shown): delete ~/erp-snapshot, then ~/.ssh/erp_github and ~/.ssh/erp_github.pub, and remove the Host github-erp block from ~/.ssh/config.
REMOVE THE ROOT JOB
Terminal
rm /etc/cron.d/erp-ftplog-USERNAME
Important
Warning: ~/erp-snapshot holds the email password in .smtp. Do not leave it behind.