Administration and operations
Updates, backups and monitoring
Deploy new versions and roll back, schedule reminders and backups, restore data, read logs, and know the platform’s limits.
On this page
Deploy an update#
The new version is built next to the running one in /var/www/boolanga-lms-new, then the folders are swapped. The app is down for a few seconds during the swap, and the previous release stays in /var/www/boolanga-lms-prev for rollback.
On your computer, upload the new version.
bashrsync -av --exclude node_modules --exclude .next --exclude data --exclude .env \ boolanga-lms/ you@your-server:/tmp/boolanga-lms-new/On the server, put it next to the running app with the current settings.
bashsudo rm -rf /var/www/boolanga-lms-new sudo mv /tmp/boolanga-lms-new /var/www/boolanga-lms-new sudo cp -p /var/www/boolanga-lms/.env /var/www/boolanga-lms-new/.env sudo chown -R lms:lms /var/www/boolanga-lms-newBack up the database.
bashsudo -u postgres sh -c 'pg_dump boolanga_lms | gzip > /var/backups/boolanga-lms/before-update-$(date +%F-%H%M).sql.gz'Install, build and migrate while the current version keeps running.
bashsudo -iu lms bash -c 'cd /var/www/boolanga-lms-new && npm ci && npm run build && npm run db:migrate'If this fails, users aren’t affected yet. Fix the problem and run it again.
Swap the folders and start the new version.
bashsudo -iu lms pm2 delete boolanga-lms sudo rm -rf /var/www/boolanga-lms-prev sudo mv /var/www/boolanga-lms /var/www/boolanga-lms-prev sudo mv /var/www/boolanga-lms-new /var/www/boolanga-lms sudo -iu lms bash -c 'cd /var/www/boolanga-lms && pm2 start ecosystem.config.cjs && pm2 save'Check it.
sudo -iu lms pm2 logs boolanga-lms --lines 50shows no errors, the admin panel signs in, and the rider sign-in page loads.
Update at a quiet time
before-update-*.sql.gz files aren’t removed by the nightly clean-up. Delete old ones by hand.
Once, after the update with the assignment-completion fix
Run this one time to mark assignments that got stuck before the fix as complete. It only completes assignments whose courses are all done, and running it again changes nothing else:
sudo -iu lms bash -c 'cd /var/www/boolanga-lms && npm run db:refresh-completions'Roll back an update#
sudo -iu lms pm2 delete boolanga-lms
sudo mv /var/www/boolanga-lms /var/www/boolanga-lms-failed
sudo mv /var/www/boolanga-lms-prev /var/www/boolanga-lms
sudo -iu lms bash -c 'cd /var/www/boolanga-lms && pm2 start ecosystem.config.cjs && pm2 save'Database changes
before-update-* backup before starting the previous version.Scheduled jobs#
Two jobs run from cron. Neither crontab contains a password.
| Job | Runs as | When | Output |
|---|---|---|---|
| Reminders for unfinished training | lms | Daily 09:00 server time | /var/log/rider-academy-reminders.log |
| Database backup | postgres | Daily 02:30, kept 14 days | /var/backups/boolanga-lms/ |
Reminders: create the log file for lms, then edit its crontab.
sudo touch /var/log/rider-academy-reminders.log
sudo chown lms:lms /var/log/rider-academy-reminders.log
sudo crontab -u lms -e# Reminders for unfinished training, every day at 09:00 server time
0 9 * * * cd /var/www/boolanga-lms && /usr/bin/npm run reminders >> /var/log/rider-academy-reminders.log 2>&1Backups: create the backup folder for postgres (mode 700), then edit its crontab. postgres connects to the database without a password.
sudo install -d -o postgres -g postgres -m 700 /var/backups/boolanga-lms
sudo crontab -u postgres -e# Nightly database backup at 02:30, kept for 14 days
30 2 * * * pg_dump boolanga_lms | gzip > /var/backups/boolanga-lms/boolanga_lms_$(date +\%F).sql.gz && find /var/backups/boolanga-lms -name 'boolanga_lms_*.sql.gz' -mtime +14 -delete- The reminders job must
cdinto the app folder: the script reads.envfrom the current folder. - In a crontab,
%must be written as\%. - Check the server time zone with
timedatectl, and list the jobs withsudo crontab -u lms -landsudo crontab -u postgres -l. - A reminders run logs a line like
Reminded 12 riders (0 delivered on WhatsApp).See Reminders for who gets one.
Backups#
| What | Why | How |
|---|---|---|
| PostgreSQL database | Everything: riders, courses, records, flags, knowledge documents, admin accounts. | The nightly backup job above. |
.env | The secrets. Without the same PIN_SECRET (or SESSION_SECRET), restored rider PINs don’t work. | Store a copy in a password manager or secrets vault, not next to the database backups. |
| App code | Can be deployed again from the repository. | Keep the release you run tagged in Git. |
Uploaded PDFs and SCORM files aren’t stored: only the course text made from them, which is in the database.
Keep copies off the server, and test restores
/var/backups/boolanga-lms daily to storage in another location (in the same region if data residency requires it), and restore one on a test server from time to time.Restore from a backup#
Stop the app.
bashsudo -iu lms pm2 stop boolanga-lmsRecreate an empty database.
bashsudo -u postgres psql -c "DROP DATABASE boolanga_lms WITH (FORCE);" sudo -u postgres psql -c "CREATE DATABASE boolanga_lms OWNER lms;"Load the backup. List the files with sudo ls /var/backups/boolanga-lms.
bashsudo -u postgres sh -c 'gunzip -c /var/backups/boolanga-lms/boolanga_lms_2026-09-17.sql.gz | psql -q -d boolanga_lms'If the app version is newer than the backup, apply its migrations.
bashsudo -iu lms bash -c 'cd /var/www/boolanga-lms && npm run db:migrate'Start the app and check it.
bashsudo -iu lms pm2 start boolanga-lms
.env must have the same PIN_SECRET (or, without it, SESSION_SECRET) as when the backup was taken, or rider PINs won’t work.
Logs and monitoring#
| What | Where |
|---|---|
| App output and errors | sudo -iu lms pm2 logs boolanga-lms, files in /home/lms/.pm2/logs/ |
| Process state, memory, restarts | sudo -iu lms pm2 status, sudo systemctl status pm2-lms |
| Web requests | /var/log/nginx/access.log, /var/log/nginx/error.log |
| Reminders | /var/log/rider-academy-reminders.log |
| Blocked SSH addresses | sudo fail2ban-client status sshd |
| HTTPS certificate and renewals | sudo certbot certificates, systemctl list-timers snap.certbot.renew.timer, sudo certbot renew --dry-run |
| Database | /var/log/postgresql/ |
| Messages sent to riders | Each rider page, under Send course link (last 10), or the notifications table. |
Errors worth watching for in the app log:
Course generation failed,Translation failed: AI problems (key, rate limits, material).Assistant stream failed: the assistant couldn’t answer.SESSION_SECRET must be set: the secret is missing or shorter than 16 characters.
- PM2 restarts the app if it uses more than 1 GB of memory. Frequent restarts show in
sudo -iu lms pm2 status. - PM2 logs grow without limit. Install log rotation once:
sudo -iu lms pm2 install pm2-logrotate. - There is no health-check endpoint. The sign-in pages load even when the database is down, so for uptime monitoring call
GET /api/v1/completions?limit=1with the API key: it answers 200 only when the database works.
To list failed WhatsApp sends with their error text (the admin panel only shows “failed”):
sudo -u postgres psql -d boolanga_lms -c "select created_at, kind, error from notifications where status = 'failed' order by created_at desc limit 20;"Limits to know#
| What | Limit |
|---|---|
| Course source file (PDF, SCORM, text) | 20 MB |
| Rider CSV import | 25 MB per file |
| Assistant knowledge document | 20 MB |
| POST /api/v1/riders | 5,000 riders per call |
| POST /api/v1/assignments | 250,000 rider IDs per call |
| GET /api/v1/completions | 5,000 records per page |
| Reminders | 5,000 assignments per run |
| CSV exports | 500,000 rows per file |
| Assistant | 40 questions per rider per hour |
| HTTPS certificate | About 6 days for the IP certificate, 90 days on a domain; renewed automatically by certbot’s timer |
| Sign-in attempts | 5 per rider ID or admin email per 15 minutes |
| AI jobs | A course generation or translation still running after about 15 minutes is marked failed |
| Scale | Assignment uses bulk SQL, but the platform hasn’t been load-tested with 200,000 riders yet. |
Useful commands#
| Command | What it does |
|---|---|
sudo -iu lms pm2 restart boolanga-lms | Restart the app, for example after changing .env. |
sudo -iu lms pm2 logs boolanga-lms | Follow the app log. |
sudo -iu lms bash -c 'cd /var/www/boolanga-lms && npm run reminders' | Send due reminders now (same as the cron job). |
npm run db:migrate | Apply new database migrations (as lms, in the app folder). |
npm run db:seed -- --admin-only | Create the first admin on an empty database. |
npm run db:refresh-completions | Mark finished assignments complete. A one-off after the completion fix; safe to repeat. |
npm run db:seed -- --reset | Deletes all data and loads the demo data. Never on a live platform. |
npm run db:seed -- --reset empties every table, including riders, records and admin accounts.