Fix MEDIUM findings from 2026-09-24 maintenance review

- Backend retries DB connection at startup (up to 180s) so host reboots
  no longer crash-loop it; add backend and frontend healthchecks
- Docker log rotation (json-file 10m x 3) on all services
- ntfy alerts use X-Real-IP (set by nginx after real_ip resolution)
  instead of the client-controlled first X-Forwarded-For entry
- Frontend build on Node 24 LTS with package-lock.json + npm ci;
  axios 1.20.0, vite 5.4.21
- README: backup/restore/rollback runbook, real-IP proxy trust notes
- Release-Notes/v1.1.md; version 1.1.0

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
derekcandClaude Opus 5.5 committed 2026-09-24 23:44:25 -07:00
1 parent 3170c7f4eb
commit 4130467b22
11 files changed
+1851 -20

No files matched your search

+71 -1
View File
@@ -49,7 +49,7 @@ A self-hosted web app for managing homeschool schedules, tracking daily learning
| Frontend server | nginx (Docker) |
| Backend API | FastAPI (Python 3.12) |
| Real-time | WebSockets via FastAPI |
| Database | MySQL 8 |
| Database | MySQL 8.4 LTS |
| ORM | SQLAlchemy 2.0 (async) |
| Auth | JWT — PyJWT + passlib/bcrypt |
| Orchestration | Docker Compose |
@@ -374,6 +374,8 @@ Nginx enforces the following rate limits (per IP):
Requests over the limit receive a `429 Too Many Requests` response.
Limits are keyed on the real client IP. `frontend/nginx.conf` trusts the reverse proxy and Cloudflare (`set_real_ip_from`) and walks `X-Forwarded-For` past them. **If the NPM host IP changes, or you front the app with a different proxy/CDN, update those `set_real_ip_from` lines.** Otherwise every request appears to come from the proxy and all clients share one limit. The backend reads the resolved IP from `X-Real-IP` for ntfy alerts.
### Login Lockout
After 5 consecutive failed login attempts, a parent account is locked for 15 minutes. The lock clears automatically after the cooldown, or immediately when a super admin resets the user's password. The lockout threshold and duration are configured in `backend/app/routers/auth.py` (`_LOGIN_MAX_ATTEMPTS`, `_LOGIN_LOCKOUT_MINUTES`).
@@ -411,3 +413,71 @@ docker compose down -v
# Restart without rebuilding
docker compose up
```
---
## Backup, Restore & Rollback
All persistent state is in the MySQL volume (`homeschool_mysql_data`). The live `.env` is not in git, so keep a copy somewhere safe.
### Back up the database
```bash
mkdir -p ~/backups/homeschool
docker exec homeschool_db sh -c 'exec mysqldump -uroot -p"$MYSQL_ROOT_PASSWORD" \
--single-transaction --routines --triggers --events --databases "$MYSQL_DATABASE"' \
| gzip > ~/backups/homeschool/homeschool-$(date +%Y%m%d-%H%M).sql.gz
```
Copy the dump **off the Docker host**. A backup on the same disk doesn't survive a host failure.
### Restore the database from a dump
```bash
docker compose stop backend frontend
zcat ~/backups/homeschool/<file>.sql.gz | \
docker exec -i homeschool_db sh -c 'exec mysql -uroot -p"$MYSQL_ROOT_PASSWORD"'
docker compose start backend frontend
```
The dump includes `CREATE DATABASE`/`DROP TABLE` statements, so it replaces the current tables.
### Before an update: record a rollback point
```bash
# App images are built locally and tagged :latest, so a rebuild overwrites them
docker tag homeschool-backend homeschool-backend:rollback-$(date +%Y%m%d)
docker tag homeschool-frontend homeschool-frontend:rollback-$(date +%Y%m%d)
# Take a database backup (above). For a MySQL version upgrade, also copy the volume,
# because MySQL can't be downgraded in place:
docker compose stop
docker volume create homeschool_mysql_data_pre_<date>
docker run --rm -v homeschool_mysql_data:/from:ro -v homeschool_mysql_data_pre_<date>:/to \
alpine sh -c 'cp -a /from/. /to/'
```
### Roll back an app update
```bash
docker tag homeschool-backend:rollback-<date> homeschool-backend:latest
docker tag homeschool-frontend:rollback-<date> homeschool-frontend:latest
docker compose up -d --no-build
```
If the update changed the schema, restore the pre-update dump too (above).
### Roll back a MySQL version upgrade
1. `git checkout` the previous `image: mysql:…` line in `docker-compose.yml`.
2. `docker compose stop`.
3. Copy the pre-upgrade volume back over `homeschool_mysql_data`:
```bash
docker run --rm -v homeschool_mysql_data_pre_<date>:/from:ro -v homeschool_mysql_data:/to \
alpine sh -c 'rm -rf /to/* && cp -a /from/. /to/'
```
Or recreate the volume and restore the dump.
4. `docker compose up -d`.
### Releases
Deployed versions are tagged `vX.Y.Z` in git, with notes in `Release-Notes/vX.Y.md`. To see what's running, compare `docker image inspect homeschool-backend --format '{{.Created}}'` to the tag date.