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:
1 parent
3170c7f4eb
commit
4130467b22
11 files changed
+1851
-20
No files matched your search
@@ -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.
|
||||
Reference in new issue
Block a user