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.
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.
| Part | Runs as | File | When |
|---|---|---|---|
| Snapshot | the account | ~/erp-snapshot/snapshot.sh | every 5 minutes |
| Nightly report | the account | ~/erp-snapshot/daily_report.sh | 23:30 |
| FTP log copy | root | /usr/local/sbin/erp-ftplog-account.sh | every 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:
| Placeholder | Meaning | Example |
|---|---|---|
| USERNAME | the cPanel username | erpnasirgroup |
| DOMAIN | the account's main domain | erp.nasirgroup.org |
| OWNER/REPO | the GitHub repository | erpcombd/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:
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.
- 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) - 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.
- Set permissions (right-click → Change Permissions):config.sh, snapshot.sh, daily_report.sh: 711
.smtp: 600
2.2 Edit config.sh#
Five lines change. Open config.sh with Edit in File Manager and set them to:
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"| Line | Rule |
|---|---|
| APP_DIR | No / at the end: a trailing slash makes every change show as main login. Never the whole home folder /home/USERNAME. |
| BASE_DIR | Always /home/USERNAME/erp-snapshot. |
| GITHUB_REMOTE | Must start with git@github-erp:. With git@github.com: every push is refused. |
| GITHUB_WEB | No .git at the end, or the link in the email breaks. |
| REPORT_LABEL | Add 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.
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:
whoami
cd ~/erp-snapshot
ps -u $(whoami) -o args | grep '[g]it '
ls -d repo.git 2>/dev/nullwhoami must print USERNAME. The two lines after cd must print nothing. Then start the build:
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:
cat ~/erp-snapshot/baseline.out
du -sh ~/erp-snapshot/repo.gitWait 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.
3.2 Check the history#
Nothing goes to GitHub until this check passes, because a pushed history cannot be taken back.
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| Command | Must show |
|---|---|
| log | baseline: production state |
| ls-files | wc -l | the number of tracked files |
| folder list | only the folders of what you meant to watch. Names such as mail, logs, erp-snapshot or other domains mean APP_DIR is wrong. |
| MASKED count | a 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:
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.pubOn 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:
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-erpType yes if asked to continue connecting. It must answer Hi OWNER/REPO! You've successfully authenticated.
3.4 First push#
The history goes to GitHub once now; after this the nightly report pushes it automatically.
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:
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.
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:
whoami
ls -la /usr/local/sbin/erp-ftplog-account.shIf it says "No such file", install it:
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.shU=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:
ls /usr/local/apache/domlogs/ | grep ftp5 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:
cd ~/erp-snapshot && bash daily_report.sh; tail -2 snapshot.logThe 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:
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')"| Line | Healthy |
|---|---|
| cron lines | exactly two per account: snapshot.sh and daily_report.sh |
| log copy refreshed | within the last minute of time now |
| last snapshot run | within the last 5 minutes (hourly for mhafuzdev); 20261026071502 means 07:15:02 on 26 October |
| last log line | the latest snapshot committed or report sent |
5.2 Developer FTP accounts#
Names appear only for changes made through a personal FTP account; until then every change is labelled main login.
- 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.
- 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).
- Change the cPanel password and keep it private. Anyone who has it can still edit as main login.
5.3 Troubleshooting#
Most of these came up while setting up the first accounts; each has a one-line fix.
| Symptom | Cause | Fix |
|---|---|---|
| Permission denied (publickey) on push; email subject [PUSH FAILED] | GITHUB_REMOTE uses github.com | fix it in config.sh, then run g remote set-url origin "$GITHUB_REMOTE" after source config.sh |
| ssh -T greets a different repository | the key was added to the wrong repository | delete it there, add it to OWNER/REPO |
| Every change labelled main login | APP_DIR ends in /, or no developer FTP accounts yet | remove the /; create the developer FTP accounts |
| Building the history runs for a very long time | APP_DIR is the whole home folder | kill the git processes, rename repo.git, fix APP_DIR, build the history again |
| Two git add processes in ps | the build was started twice | kill both, rename repo.git, start once |
| Two emails a night from one account | cron jobs added twice | as root, remove the duplicate lines with the command below |
| su: command not found | you are inside a cPanel account | use WHM → Terminal for root steps |
| cd: /home/OTHER/...: No such file | an account cannot see another account's folders | run that step as root |
| last snapshot run never changes | no cron jobs, or scripts lack permission 711 | add the account's cron jobs; set the permissions from Copy the files |
| log copy refreshed is old | no root job for the account | set up root's copy job |
| FTP log not available, no names in snapshot.log | the log copy stopped refreshing | as root, run root's copy script by hand and read its error |
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.
- 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.
- Account cron jobs (manual): delete the two erp-snapshot lines on the account's Cron Jobs page.
- Root job (root terminal): remove the account's schedule file, never /etc/cron.d/erp-ftplog, which belongs to mhafuzdev.
- 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.
rm /etc/cron.d/erp-ftplog-USERNAME