Create /data/notes in the image, and write the rollout runbook
The Dockerfile creates /data/mirror and /data/state with the right owner, and the comment above it says exactly why: Docker initialises a new named volume from the image directory, so a mount point the image does not have lands root-owned and the unprivileged user gets EACCES on every write. /data/notes was added to docker-compose.yml without being added here, so every save on a fresh deployment would have failed that way — verified both directions before fixing it. docs/DEPLOY-NOTES.md is the runbook for putting this on a server that is already running: publish, decide where the notes live *before* anything writes one, set WEB_PASSWORD, verify, migrate, index. Plus rollback, which is uneventful — no migration, and the old image simply ignores the new settings and leaves the notes volume alone. PI.md's backup table needed the bigger change. Everything else this server stores is a copy of something upstream and a crawl rebuilds it; the notes are not, and nothing can. They are now the one entry in that table marked irreplaceable, and the EACCES row says to fix the volume's ownership rather than delete it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
42
docs/PI.md
42
docs/PI.md
@@ -88,6 +88,17 @@ INDEX_PERSONAL_FILES=true
|
||||
EOF
|
||||
```
|
||||
|
||||
To write your own lesson notes — and to replace the Schulcloud token from a
|
||||
phone rather than a terminal — set a password for the app at `/app`. Unlike the
|
||||
tokens above it is typed by a person, so it is a passphrase: **at least 12
|
||||
characters, three or four words**. See [NOTES.md](NOTES.md).
|
||||
|
||||
```bash
|
||||
cat >> .env <<'EOF'
|
||||
WEB_PASSWORD=change-this-to-three-or-four-words
|
||||
EOF
|
||||
```
|
||||
|
||||
If your school publishes its timetable in **WebUntis**, add those four values
|
||||
too — the timetable, its cancellations and substitutions are not in Schulcloud
|
||||
at all. They are in WebUntis under Profil → Freigaben → Untis Mobile → QR-Code:
|
||||
@@ -112,13 +123,15 @@ What those lines do:
|
||||
| `MCP_CONNECTOR_TOKEN` | What claude.ai sends as a request header. It opens `/mcp` only, never `/api`, because claude.ai stores it. |
|
||||
| `INDEX_PERSONAL_FILES` | Also indexes your own files and handed-in work, including teachers' feedback. Optional. |
|
||||
| `UNTIS_*` | WebUntis, where the school keeps the timetable. All four or none; the key needs no password and does not expire. See [AUTH.md](AUTH.md). Optional. |
|
||||
| `WEB_PASSWORD` | The app at `/app`: the notes editor and a settings page for the Schulcloud token. Unset means no app is served at all. Hashed at startup and never logged; changing it logs out every session. Optional. |
|
||||
| `SCHULCLOUD_MCP_TAG` | Which published image to run. Unset means `latest`; a commit id such as `bac9130` pins it, so updates happen only when you change it. Optional. |
|
||||
|
||||
**Copy `MCP_AUTH_TOKEN` and `MCP_CONNECTOR_TOKEN` into your password manager
|
||||
now** — you need both again in step 9, and neither is ever printed by the server:
|
||||
**Copy `MCP_AUTH_TOKEN`, `MCP_CONNECTOR_TOKEN` and `WEB_PASSWORD` into your
|
||||
password manager now** — you need the first two again in step 9, and none of
|
||||
them is ever printed by the server:
|
||||
|
||||
```bash
|
||||
grep -E '^(MCP_AUTH_TOKEN|MCP_CONNECTOR_TOKEN)=' .env
|
||||
grep -E '^(MCP_AUTH_TOKEN|MCP_CONNECTOR_TOKEN|WEB_PASSWORD)=' .env
|
||||
```
|
||||
|
||||
## 4. The first Schulcloud token
|
||||
@@ -406,13 +419,27 @@ Going back works the same way: set the previous commit id, then `pull` and
|
||||
|
||||
## Backups
|
||||
|
||||
Everything this server stores is a copy of something upstream — with one
|
||||
exception. **Your own notes are not a copy of anything**, and nothing can
|
||||
rebuild them.
|
||||
|
||||
| What | Needed? |
|
||||
|---|---|
|
||||
| `.env` | **Yes** — it holds every secret. Only ever back it up encrypted. |
|
||||
| **`schulcloud-mcp_notes`** | **Yes. The only irreplaceable thing here** — a note taken in a lesson cannot be retaken. Small: a school year is a few megabytes. |
|
||||
| Postgres | Optional: a crawl rebuilds it. `docker compose exec postgres pg_dump -U schulcloud schulcloud \| gzip > index.sql.gz` |
|
||||
| `schulcloud-mcp_mirror` | No — re-downloaded by the next crawl. |
|
||||
| `schulcloud-mcp_state` | No — a replaced token, expiring within 30 days anyway. |
|
||||
|
||||
```bash
|
||||
docker run --rm -v schulcloud-mcp_notes:/notes:ro -v "$PWD":/out alpine \
|
||||
tar czf /out/notizen-$(date +%F).tar.gz -C /notes .
|
||||
```
|
||||
|
||||
If you bind-mounted a directory you already sync ([NOTES.md](NOTES.md)), back
|
||||
that up instead — and check the sync tool keeps versioned copies rather than
|
||||
assuming it does.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Symptom | Likely cause | Fix |
|
||||
@@ -431,7 +458,10 @@ Going back works the same way: set the previous commit id, then `pull` and
|
||||
| Compose rejects `!reset` in `deploy/docker-compose.pi.yml` | Compose older than 2.24 | Update Docker (step 1) |
|
||||
| `WebUntis rejected the server's key` from a untis_* tool | The key was regenerated in WebUntis, or `UNTIS_USER` does not match it | Copy both again from Profil → Freigaben → Untis Mobile, then `docker compose up -d --force-recreate schulcloud-mcp` |
|
||||
| `the server's clock is too far off` from a untis_* tool | The Pi's clock has drifted; the Untis code is time-based | `timedatectl status`, then fix NTP |
|
||||
| `EACCES` for `/data/state` or `/data/mirror` in the logs | A volume created by an old image, owned by root | `docker compose down`, `docker volume rm schulcloud-mcp_state` (or `_mirror`), `docker compose up -d` |
|
||||
| `EACCES` for `/data/state`, `/data/mirror` or `/data/notes` in the logs | A volume created by an old image, owned by root | `docker compose down`, `docker volume rm schulcloud-mcp_state` (or `_mirror`) and `docker compose up -d`. **Never delete `_notes`** — back it up, then fix the ownership: `docker run --rm -v schulcloud-mcp_notes:/n alpine chown -R 1000:1000 /n` |
|
||||
| `/app/` answers 404 | `WEB_PASSWORD` not set, or the container not recreated since it was | `grep ^WEB_PASSWORD= .env`, then `docker compose up -d --force-recreate schulcloud-mcp` |
|
||||
| The app logs you out constantly | The session cookie is marked `Secure` but the connection is not HTTPS, or Caddy's `X-Forwarded-Proto` is being stripped | Reach it over HTTPS; leave that header alone |
|
||||
| Notes exist but `search` cannot find them | Only a **full** crawl reads them | `schulcloud refresh --force` |
|
||||
|
||||
## Security checklist
|
||||
|
||||
@@ -451,3 +481,7 @@ Going back works the same way: set the previous commit id, then `pull` and
|
||||
`docker compose up -d --force-recreate schulcloud-mcp`, and re-add the
|
||||
connector with the new header. A leaked secret path is replaced the same way.
|
||||
`MCP_AUTH_TOKEN` stays valid either way.
|
||||
- `WEB_PASSWORD` is the one credential here that can be guessed, so its length
|
||||
is what protects the notes and the Schulcloud token. Changing it and
|
||||
recreating the container logs out every session — that is the revocation
|
||||
mechanism, and the thing to do if a phone is lost.
|
||||
|
||||
Reference in New Issue
Block a user