Backup, restore, deployment — operations
PHP 8.2 + MariaDB 10.4 + Apache. Nightly backup cron writes db.sql.gz + storage.tar.gz with SHA-256 manifest. 7-day local rotation. Off-site rclone sync to B2/S3/Wasabi. Quarterly restore drills. /health endpoint for monitoring. Full deployment guide in docs/deployment/README.md.
Stack requirements
| PHP 8.2.12+ | strict_types=1 in every file · 8.2-compat syntax (avoid 8.3-only) |
| MariaDB 10.4+ | or MySQL 8.0+ · utf8mb4 · use prepared statements |
| Apache 2.4 + mod_rewrite | or nginx + PHP-FPM |
| PHP extensions | pdo_mysql · mbstring · gd · openssl · curl · zip · bcmath · phar · json · session |
| Composer 2.x | For PSR-4 autoload + dev tools |
| rclone | For off-site backup sync (optional but recommended) |
First-time install
Clone repo + composer install
git clone ... && cd acwms-v5 && composer install --no-devCreate database
CREATE DATABASE acwms_v5 CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;Apply migrations
php bin/migrate.phpCreate config files
Copy
.env.example→.env· fill DB credentials. Copyconfig/database.local.php.example→config/database.local.php+config/communication.local.php.example→config/communication.local.php. Set permschmod 0600.Apache vhost
DocumentRoot must point to
public/only. Nothing above public/ should be web-accessible.Verify
Hit
/health— expect HTTP 200 with status=ok in < 50ms.Verify isolation
Curl
/composer.lock·/storage/sessions/·/config/database.local.php— all should return 404 (proves DocumentRoot isolation).Storage permissions
Web user (e.g.
www-data) needs write tostorage/tree (NOT toconfig/).
The 7 production crons
| Cron | Schedule | Purpose |
|---|---|---|
cron-backup.php | Daily 00:00 | db.sql.gz + storage.tar.gz + manifest |
cron-offsite-backup.sh | Daily 00:30 | rclone sync to remote |
cron-m17-email-queue.php | Every 5 min | Process pending email queue |
cron-m17-expiry-reminders.php | Daily 06:00 | Doc/visa/deadline reminders |
cron-m20-deadline-generator.php | Daily 01:30 | Roll 12-month forward window |
cron-m16-retention.php | Daily 00:00 | Vault retention purge |
cron-m13-eosb-accrual.php | Last day of month 23:30 | EOSB monthly snapshot |
cron-m13-leave-carry-forward.php | 1 January 02:00 | Leave year-end carry-forward |
Linux crontab
# AuditPro Suite production crons 0 0 * * * /usr/bin/php /var/www/acwms-v5/bin/cron-backup.php > /var/log/acwms/backup.log 2>&1 30 0 * * * /var/www/acwms-v5/bin/cron-offsite-backup.sh > /var/log/acwms/offsite.log 2>&1 */5 * * * * /usr/bin/php /var/www/acwms-v5/bin/cron-m17-email-queue.php > /var/log/acwms/email.log 2>&1 0 6 * * * /usr/bin/php /var/www/acwms-v5/bin/cron-m17-expiry-reminders.php > /var/log/acwms/expiry.log 2>&1 30 1 * * * /usr/bin/php /var/www/acwms-v5/bin/cron-m20-deadline-generator.php > /var/log/acwms/m20.log 2>&1 0 0 * * * /usr/bin/php /var/www/acwms-v5/bin/cron-m16-retention.php > /var/log/acwms/retention.log 2>&1 30 23 28-31 * * /usr/bin/php /var/www/acwms-v5/bin/cron-m13-eosb-accrual.php > /var/log/acwms/eosb.log 2>&1 0 2 1 1 * /usr/bin/php /var/www/acwms-v5/bin/cron-m13-leave-carry-forward.php > /var/log/acwms/leave.log 2>&1
Backup contents
Each nightly backup at storage/backups/{YYYY-MM-DD}/:
- db.sql.gz — gzip-compressed mysqldump --single-transaction --routines --triggers --events --hex-blob
- storage.tar.gz — PharData of clients/jobs/documents/company/employees/payments/quotes/expenses/bank-statements
- manifest.json — sha256 + size per file + app version
7-day local rotation. Cross-platform (Linux + Windows).
Off-site backup
cron-offsite-backup.sh wraps rclone:
- Targets: Backblaze B2 / AWS S3 / Wasabi / rsync
- Pre-flight checks before sync
- Stamped logging
- 3-2-1 rule: 3 copies, 2 media, 1 off-site
Setup is documented in docs/deployment/backup-and-security-plan.md.
Restore from backup
Pull from off-site
rclone copy {remote}:backups/{YYYY-MM-DD}/ ./restore/Verify SHA-256
sha256sum *.gz· compare to manifest.jsonRestore DB
gunzip < db.sql.gz | mysql acwms_v5Restore storage
tar xzf storage.tar.gz -C ./(after backup of current storage)Verify
Hit /health · open a known-good engagement · check archive integrity
Quarterly restore drill
Every quarter:
- Pick a random backup from off-site
- Restore to a sandbox environment
- Verify SHA-256 matches manifest
- Open the restored engagement · verify report PDF + workpapers + audit log
- Log the drill result in
backup_drillstable
Backup that hasn't been tested = hope, not preparedness.
Health monitoring
Plug /health into:
- Datadog / Pingdom / Uptime Kuma — alerts on 503 + status ≠ ok
- Email alert thresholds:
- Critical: status=fail or response > 5s
- Warning: status=warning or email_queue_stale > 100
- Slack / WhatsApp / SMS for partner notifications
Hardening checklist
- HTTPS only · HSTS header
- Apache + PHP version disclosure off (
ServerSignature Off,expose_php Off) - file modes
0644for files ·0755for dirs ·0600for *.local.php - logrotate for
/var/log/acwms/ - fail2ban for SSH
- UFW or iptables — open only 80, 443, 22
- SSH key auth only (disable password)
- DB user least privilege — no root
- Quarterly security review (chapter 23)
Run php bin/cron-backup.php manually. Within ~10s, today's backup folder appears at storage/backups/{YYYY-MM-DD}/. Inspect manifest.json. Verify SHA-256 of db.sql.gz matches what's recorded. This is your verifiable disaster-recovery proof.
If off-site sync fails 2 nights in a row, you're one disk-failure away from data loss. Configure paging on cron failures. Audit firms have lost their licence over evidence-file destruction. Backup is non-negotiable.
Create a dedicated MySQL user with only SELECT, LOCK TABLES, and EVENT permissions for the backup cron. mysqldump doesn't need INSERT/UPDATE/DELETE. Reduces blast radius if the backup credentials leak.