diff --git a/HOMELAB_ARCHITECTURE_MASTER_V2.md b/HOMELAB_ARCHITECTURE_MASTER_V2.md index fcbea38..719e75e 100644 --- a/HOMELAB_ARCHITECTURE_MASTER_V2.md +++ b/HOMELAB_ARCHITECTURE_MASTER_V2.md @@ -89,6 +89,7 @@ Jeder produktive Container nutzt `restart: unless-stopped`, außer eine Ausnahme | `dns_net` | bridge | Resolver-Schicht: AdGuard Home + Unbound | bleibt | | `mealie_mealie_internal` | bridge, `internal: true` | internes Netz nur für `mealie` + `mealie-postgres` | ✅ umgesetzt | | `immich_default` | Compose-intern, `internal: true` | internes Immich-Netz | ✅ umgesetzt | +| `nextcloud_internal` | bridge, `internal: true` | internes Netz nur fuer `nextcloud` + `nextcloud-postgres` + `nextcloud-redis` | ✅ vorbereitet | | `host` | host | nur für echte Sonderfälle | begründet | ### 3.2 Finales Diagramm (vereinfacht) @@ -99,8 +100,8 @@ Internet traefik (80/443) │ └── frontend_net - ├── öffentliche Apps (vaultwarden, mealie, paperless, immich, gitea, ntfy, mail-archiver) - ├── geschützte UIs mit Middleware (homepage, paperless-gpt, uptime-kuma, filebrowser, scrutiny, code-server, backrest, borg-ui, glances, speedtest) + ├── öffentliche Apps (vaultwarden, mealie, paperless, immich, gitea, ntfy, mail-archiver, nextcloud) + ├── geschützte UIs mit Middleware (homepage, paperless-gpt, uptime-kuma, filebrowser, scrutiny, code-server, backrest, borg-ui, glances, speedtest, stirling-pdf) ├── Admin-UI mit nativer Auth (komodo) └── Dienste mit Internetbedarf ohne öffentliche UI (ddns-updater) @@ -116,7 +117,8 @@ dns_net App-interne Netze ├── mealie_mealie_internal (internal: true) ✅ -└── immich_default (internal: true) ✅ +├── immich_default (internal: true) ✅ +└── nextcloud_internal (internal: true) ✅ Host-Sonderfälle ├── tailscale @@ -138,6 +140,7 @@ Diese Dienste sind über echte `*.kaleschke.info`-Domains erreichbar: - `gitea` (Web) — git.kaleschke.info - `immich_server` — immich.kaleschke.info - `mail-archiver` — mail.kaleschke.info +- `nextcloud` — cloud.kaleschke.info ### 4.2 Nicht öffentlich / nur Tailscale oder Traefik + Middleware Diese Dienste sind **keine Public Apps**: @@ -153,6 +156,7 @@ Diese Dienste sind **keine Public Apps**: - `paperless-gpt` — paperless-gpt.kaleschke.info (Middleware) - `glances` — glances.kaleschke.info (Middleware) - `speedtest-tracker` — speedtest.kaleschke.info (Middleware) +- `stirling-pdf` — pdf.kaleschke.info (Middleware) - `Traefik-Dashboard` - `AdGuard Home` — Port 3000 direkt (kein Traefik, nur LAN-Zugang) @@ -249,6 +253,8 @@ Legende Status: | `mealie-postgres` | ✅ | `mealie_mealie_internal` | intern | isoliert, nie `frontend_net` | — | | `immich_postgres` | ✅ | `immich_default` | intern | intern-only | — | | `immich_redis` | ⏳ | `immich_default` | intern | intern-only | anonymes Volume → named volume | +| `nextcloud-postgres` | ✅ | `nextcloud_internal` | intern | app-eigene Nextcloud-Datenbank mit `_FILE`-Secret | — | +| `nextcloud-redis` | ✅ | `nextcloud_internal` | intern | app-eigener Cache fuer File Locking / Sessions | — | ### 7.4 Öffentliche Apps @@ -261,6 +267,7 @@ Legende Status: | `gitea` | ✅ | `frontend_net` | Traefik + SSH-Port 222 | Web via Traefik, SSH direkt gebunden | — | | `immich_server` | ✅ | `immich_default`, `frontend_net` | Traefik | aktiv via `immich.kaleschke.info` | — | | `immich_machine_learning` | ✅ | `immich_default` | intern | bleibt intern | — | +| `nextcloud` | ✅ | `frontend_net`, `nextcloud_internal` | Traefik | aktiv via `cloud.kaleschke.info`, nativer Nextcloud-Login, WebDAV/CardDAV faehig | CalDAV/CardDAV-Redirect via Traefik-Labels | ### 7.5 Admin / Operations @@ -272,6 +279,7 @@ Legende Status: | `filebrowser` | ✅ | `frontend_net` | Traefik + Middleware | aktiv via `files.kaleschke.info` | Mounts einschränken (Block F) | | `borg-ui` | ✅ | `frontend_net` | Traefik + Middleware | produktiver Borg-/Restore-Dienst; `/local/secrets` ist bewusst Teil des Restore-Scopes | BorgBase-Repo und Key laufend pflegen | | `paperless-gpt` | ✅ | `frontend_net` | Traefik + Middleware | aktiv via `paperless-gpt.kaleschke.info` | — | +| `stirling-pdf` | ✅ | `frontend_net` | Traefik + Middleware | PDF-Tooling via `pdf.kaleschke.info`; lokale Stirling-Auth deaktiviert zugunsten zentraler Auth | — | ### 7.6 Monitoring / Status @@ -376,6 +384,7 @@ Für den laufenden Betrieb gilt stattdessen: | `ddns-updater` | bleibt in `frontend_net` statt `backend_net` | braucht Cloudflare-API-Zugang; `backend_net` ist `internal: true` | | `mail-archiver` | `frontend_net` + `backend_net` | braucht Internetzugang für IMAP-Abruf (GMX, Gmail) und DB-Zugang | | `traefik/dynamic/*` | manueller Host-Sync trotz GitOps | File-Provider bleibt bewusst fuer `middlewares.yml`, `tls.yml` und `dashboards.yml`; Komodo deployed diese Dateien nicht automatisch | +| `nextcloud` | keine zentrale ForwardAuth-Middleware | Nextcloud bringt eigene Auth, Clients und WebDAV/CardDAV-Endpunkte mit; Traefik bleibt Reverse Proxy, Auth bleibt app-nativ | --- @@ -508,6 +517,11 @@ Host-Pfade in `env_file` (z.B. `/mnt/...`) sind in Git-Stacks nicht verfügbar. ### Reproduzierbare Deployments (2026-04-17) Mutable Tags wie `latest`, `stable`, `release` oder reine Major-Tags wurden auf die **aktuell laufenden Digests** eingefroren. Das ist bewusst **kein Upgrade-Mechanismus**, sondern dient dazu, den heute funktionierenden Laufzeitstand exakt im Repo festzuhalten. Echte Versions-Upgrades bleiben ein eigener, geplanter Schritt. +### Nextcloud und Stirling-PDF (2026-04-19) +- `nextcloud` wird bewusst **nicht** als AIO-Stack gebaut, sondern als klassischer Docker-Microservice-Stack mit eigenem PostgreSQL und eigenem Redis. Das passt besser zum bestehenden GitOps-/Compose-Modell des Repos. +- `nextcloud` bleibt bei nativer App-Authentifizierung ohne zentrale ForwardAuth-Middleware vor dem Router, damit Browser-Login, Desktop-/Mobile-Clients sowie WebDAV/CardDAV sauber funktionieren. +- `stirling-pdf` wird als geschuetzter Tool-Stack hinter `authelia@file,secure-headers@file` betrieben; die interne Stirling-Login-Funktion bleibt deaktiviert, um Doppel-Login zu vermeiden. + ### ddns-updater — Netz-Ausnahme Bleibt bewusst in `frontend_net` statt `backend_net`, weil `backend_net` `internal: true` ist und ddns-updater die Cloudflare-API erreichen muss. diff --git a/README.md b/README.md index 0664aaa..3a0c560 100644 --- a/README.md +++ b/README.md @@ -64,3 +64,4 @@ Bei Restore-, Host-Ausfall- oder Wiederanlauf-Fragen zusaetzlich: - Mutable Image-Tags sind auf die aktuell laufenden Digests eingefroren; echte Versions-Upgrades erfolgen bewusst separat. - Disaster-Recovery und dienstspezifische Restore-Quellen sind in `docs/DISASTER_RECOVERY.md` und `docs/RESTORE_MATRIX.md` beschrieben. - Der verbindliche Detailablauf steht in `docs/WORKFLOW.md`. +- `nextcloud` und `stirling-pdf` sind repo-seitig vorbereitet und folgen dem dokumentierten Netz-/Secret-/Traefik-Modell. diff --git a/apps/nextcloud/docker-compose.yml b/apps/nextcloud/docker-compose.yml new file mode 100644 index 0000000..8c73c59 --- /dev/null +++ b/apps/nextcloud/docker-compose.yml @@ -0,0 +1,84 @@ +services: + nextcloud: + image: nextcloud:33.0.2-apache + container_name: nextcloud + restart: unless-stopped + depends_on: + - nextcloud-postgres + - nextcloud-redis + environment: + TZ: Europe/Berlin + POSTGRES_HOST: nextcloud-postgres + POSTGRES_DB: nextcloud + POSTGRES_USER: nextcloud + POSTGRES_PASSWORD_FILE: /run/secrets/postgres_password + REDIS_HOST: nextcloud-redis + NEXTCLOUD_ADMIN_USER_FILE: /run/secrets/admin_user + NEXTCLOUD_ADMIN_PASSWORD_FILE: /run/secrets/admin_password + NEXTCLOUD_DATA_DIR: /var/www/html/data + NEXTCLOUD_TRUSTED_DOMAINS: cloud.kaleschke.info + TRUSTED_PROXIES: 172.16.0.0/12 + OVERWRITEHOST: cloud.kaleschke.info + OVERWRITEPROTOCOL: https + OVERWRITECLIURL: https://cloud.kaleschke.info + volumes: + - /mnt/user/appdata/nextcloud/html:/var/www/html + - /mnt/user/documents/nextcloud-data:/var/www/html/data + - /mnt/user/appdata/secrets/nextcloud_postgres_password.txt:/run/secrets/postgres_password:ro + - /mnt/user/appdata/secrets/nextcloud_admin_user.txt:/run/secrets/admin_user:ro + - /mnt/user/appdata/secrets/nextcloud_admin_password.txt:/run/secrets/admin_password:ro + networks: + - frontend_net + - nextcloud_internal + security_opt: + - no-new-privileges:true + labels: + - "traefik.enable=true" + - "traefik.docker.network=frontend_net" + - "traefik.http.routers.nextcloud.rule=Host(`cloud.kaleschke.info`)" + - "traefik.http.routers.nextcloud.entrypoints=websecure" + - "traefik.http.routers.nextcloud.tls=true" + - "traefik.http.routers.nextcloud.tls.certresolver=le" + - "traefik.http.routers.nextcloud.middlewares=nextcloud-redirectregex" + - "traefik.http.middlewares.nextcloud-redirectregex.redirectregex.permanent=true" + - "traefik.http.middlewares.nextcloud-redirectregex.redirectregex.regex=https://(.*)/.well-known/(?:card|cal)dav" + - "traefik.http.middlewares.nextcloud-redirectregex.redirectregex.replacement=https://$${1}/remote.php/dav" + - "traefik.http.services.nextcloud.loadbalancer.server.port=80" + + nextcloud-postgres: + image: postgres:17 + container_name: nextcloud-postgres + restart: unless-stopped + environment: + TZ: Europe/Berlin + POSTGRES_DB: nextcloud + POSTGRES_USER: nextcloud + POSTGRES_PASSWORD_FILE: /run/secrets/postgres_password + PGDATA: /var/lib/postgresql/data + volumes: + - /mnt/user/appdata/nextcloud/postgres:/var/lib/postgresql/data + - /mnt/user/appdata/secrets/nextcloud_postgres_password.txt:/run/secrets/postgres_password:ro + networks: + - nextcloud_internal + security_opt: + - no-new-privileges:true + + nextcloud-redis: + image: redis:7.4-alpine + container_name: nextcloud-redis + restart: unless-stopped + command: redis-server --save 60 1 --loglevel warning + volumes: + - /mnt/user/appdata/nextcloud/redis:/data + networks: + - nextcloud_internal + security_opt: + - no-new-privileges:true + +networks: + frontend_net: + external: true + + nextcloud_internal: + driver: bridge + internal: true diff --git a/apps/stirling-pdf/docker-compose.yml b/apps/stirling-pdf/docker-compose.yml new file mode 100644 index 0000000..3ed29c9 --- /dev/null +++ b/apps/stirling-pdf/docker-compose.yml @@ -0,0 +1,35 @@ +services: + stirling-pdf: + image: stirlingtools/stirling-pdf:2.9.2 + container_name: stirling-pdf + restart: unless-stopped + environment: + DISABLE_ADDITIONAL_FEATURES: "false" + SECURITY_ENABLELOGIN: "false" + LANGS: de_DE + SYSTEM_DEFAULTLOCALE: de-DE + SYSTEM_GOOGLEVISIBILITY: "false" + SYSTEM_ROOTURIPATH: / + volumes: + - /mnt/user/appdata/stirling-pdf/tessdata:/usr/share/tessdata + - /mnt/user/appdata/stirling-pdf/configs:/configs + - /mnt/user/appdata/stirling-pdf/logs:/logs + - /mnt/user/appdata/stirling-pdf/customFiles:/customFiles + - /mnt/user/appdata/stirling-pdf/pipeline:/pipeline + networks: + - frontend_net + security_opt: + - no-new-privileges:true + labels: + - "traefik.enable=true" + - "traefik.docker.network=frontend_net" + - "traefik.http.routers.stirling-pdf.rule=Host(`pdf.kaleschke.info`)" + - "traefik.http.routers.stirling-pdf.entrypoints=websecure" + - "traefik.http.routers.stirling-pdf.tls=true" + - "traefik.http.routers.stirling-pdf.tls.certresolver=le" + - "traefik.http.routers.stirling-pdf.middlewares=authelia@file,secure-headers@file" + - "traefik.http.services.stirling-pdf.loadbalancer.server.port=8080" + +networks: + frontend_net: + external: true diff --git a/docs/DISASTER_RECOVERY.md b/docs/DISASTER_RECOVERY.md index 86f3341..5d465b2 100644 --- a/docs/DISASTER_RECOVERY.md +++ b/docs/DISASTER_RECOVERY.md @@ -133,6 +133,9 @@ Erwartete Basis unter `/mnt/user/appdata/secrets/`: - `immich_postgres_password.txt` - `komodo_mongo_password.txt` - `mealie_postgres_password.txt` +- `nextcloud_admin_password.txt` +- `nextcloud_admin_user.txt` +- `nextcloud_postgres_password.txt` - `postgres_password.txt` - `redis_password.txt` - `vaultwarden_admin_token.txt` @@ -247,20 +250,22 @@ Ziel: 11. `apps/immich/` 12. `apps/mealie/` 13. `apps/mail-archiver/` +14. `apps/nextcloud/` ### Stufe 5 - Restliche Apps und Ops -14. `apps/homepage/` -15. `apps/ntfy/` -16. `apps/paperless-gpt/` -17. `ops/uptime-kuma/` -18. `ops/borg-ui/` -19. `ops/backrest/` -20. `ops/filebrowser/` -21. `ops/glances/` -22. `ops/scrutiny/` -23. `ops/speedtest/` -24. `infra/ddns-updater/` +15. `apps/homepage/` +16. `apps/ntfy/` +17. `apps/paperless-gpt/` +18. `apps/stirling-pdf/` +19. `ops/uptime-kuma/` +20. `ops/borg-ui/` +21. `ops/backrest/` +22. `ops/filebrowser/` +23. `ops/glances/` +24. `ops/scrutiny/` +25. `ops/speedtest/` +26. `infra/ddns-updater/` **Regel:** Nach jeder Stufe kurz pruefen, bevor die naechste beginnt. @@ -289,6 +294,7 @@ Ziel: - Immich startet und sieht Medien - Mealie startet - Mail-Archiver startet +- Nextcloud startet und sieht Dateien ### 9.4 Backup-/Beobachtungsebene @@ -319,6 +325,18 @@ Das bedeutet: Nach einem Komodo-Neuaufbau muessen diese Werte vor dem Start des Stacks wieder gesetzt sein. +### `nextcloud` + +`nextcloud` ist bewusst kein AIO-Stack, sondern ein klassischer App-/PostgreSQL-/Redis-Stack. + +Vor dem Start muessen vorhanden sein: + +- `/mnt/user/appdata/secrets/nextcloud_admin_user.txt` +- `/mnt/user/appdata/secrets/nextcloud_admin_password.txt` +- `/mnt/user/appdata/secrets/nextcloud_postgres_password.txt` + +Zusaetzlich muss der Nutzdatenpfad `/mnt/user/documents/nextcloud-data` erreichbar sein. + ### Borg-Dumps Die Dump-Erzeugung ist host-seitig gedacht, nicht als Borg-UI-Inline-Hook. diff --git a/docs/MIGRATION_LOG.md b/docs/MIGRATION_LOG.md index e3ec84a..29c16c3 100644 --- a/docs/MIGRATION_LOG.md +++ b/docs/MIGRATION_LOG.md @@ -60,6 +60,13 @@ Dieses Dokument ist nur noch ein historischer Verlauf. Der aktuelle operative Ab - Diese Ruecknahme ist bewusst eng auf einen einzelnen defekten Pin begrenzt und aendert keine anderen Digest-Festschreibungen. - Die zwischenzeitlichen OCR-/Versions-Experimente fuer `paperless-gpt` wurden wieder auf den einfachen vorherigen Stand zurueckgenommen (`icereed/paperless-gpt:latest`, `VISION_LLM_MODEL=cnshenyang/qwen3-nothink:14b`), um den letzten bekannten Alltagszustand wiederherzustellen. +### 2026-04-19 - Nextcloud und Stirling-PDF vorbereitet + +- `apps/nextcloud/docker-compose.yml` als offizieller Docker-Microservice-Stack mit `nextcloud:apache`, eigener PostgreSQL-Datenbank und eigenem Redis vorbereitet. +- Nextcloud folgt dem Repo-Standard `frontend_net` + app-internes Netz, nutzt `_FILE`-Secrets fuer Admin- und DB-Passwort und ist bewusst **nicht** hinter zentraler ForwardAuth, damit WebDAV/CardDAV und native Clients sauber funktionieren. +- `apps/stirling-pdf/docker-compose.yml` als geschuetzter Tool-Stack hinter `authelia@file,secure-headers@file` vorbereitet. +- Stirling-PDF nutzt persistente Pfade fuer `/configs`, `/logs`, `/pipeline`, `/customFiles` und `/usr/share/tessdata`; interne Stirling-Login-Funktion bleibt zugunsten des zentralen Traefik-/Authelia-Zugangs deaktiviert. + --- ## Dauerhafte Learnings diff --git a/docs/RESTORE_MATRIX.md b/docs/RESTORE_MATRIX.md index ad52ff9..a5c141f 100644 --- a/docs/RESTORE_MATRIX.md +++ b/docs/RESTORE_MATRIX.md @@ -46,6 +46,7 @@ Sie ist die fachliche Ergaenzung zu `docs/DISASTER_RECOVERY.md`. | Mealie | Borg + Dump | `/mnt/user/appdata/mealie/data`, optional `/mnt/user/appdata/mealie/postgres` bei lokalem Share-Weiterbetrieb | `mealie.dump` | `mealie_postgres_password.txt` | `mealie-postgres`, Traefik | UI startet, Rezepte vorhanden | | Immich | Borg + Dump | `/mnt/user/photos/immich`, `/mnt/user/photos/family_archive` | `immich.dump` | `IMMICH_DB_PASSWORD`, `immich_postgres_password.txt` | `immich_postgres`, `immich_redis`, Traefik | UI startet, Medienbibliothek sichtbar | | Mail-Archiver | Borg + Shared Dump | `/mnt/user/appdata/mailarchiver/data-protection-keys` | `postgresql17-mailarchiver.dump` | `MAILARCHIVER_DB_CONNECTION`, `MAILARCHIVER_AUTH_PASSWORD` | PostgreSQL 17, Traefik | Web-UI startet, Archiv laesst sich oeffnen | +| Nextcloud | Borg + Share | `/mnt/user/appdata/nextcloud/html`, `/mnt/user/appdata/nextcloud/postgres`, `/mnt/user/appdata/nextcloud/redis`, `/mnt/user/documents/nextcloud-data` | app-eigene PostgreSQL unter `/mnt/user/appdata/nextcloud/postgres` | `nextcloud_admin_user.txt`, `nextcloud_admin_password.txt`, `nextcloud_postgres_password.txt` | `nextcloud-postgres`, `nextcloud-redis`, Traefik | Web-UI startet, Login funktioniert, Dateien sichtbar | | Homepage | Borg / Share | `/mnt/user/appdata/homepage` | keine | `HOMEPAGE_VAR_*` | Traefik, Authelia | Dashboard startet, Widgets laden | | ntfy | Borg / Share | `/mnt/user/appdata/ntfy` | keine | keine besonderen Secret-Dateien dokumentiert | Traefik | UI und Push-Endpunkt erreichbar | | Paperless-GPT | Borg / Share | `/mnt/user/appdata/paperless-gpt` | keine eigene DB | `PAPERLESS_API_TOKEN` | Traefik, Paperless | UI startet, Konfiguration vorhanden | @@ -63,6 +64,7 @@ Sie ist die fachliche Ergaenzung zu `docs/DISASTER_RECOVERY.md`. | Glances | Rebuildbar | kein kritischer Zustand | keine | keine | Traefik, Authelia | UI startet | | Scrutiny | Teilweise rebuildbar | `/mnt/user/appdata/scrutiny` falls gewuenscht | InfluxDB bewusst nicht Teil des Critical-Scope | keine | Traefik, Authelia | UI startet, Laufwerke sichtbar | | Speedtest Tracker | Share | `/mnt/user/appdata/speedtest-tracker/config` | SQLite im App-Pfad | `APP_KEY`, `ADMIN_PASSWORD` | Traefik, Authelia | UI startet | +| Stirling-PDF | Share | `/mnt/user/appdata/stirling-pdf/configs`, `/mnt/user/appdata/stirling-pdf/logs`, `/mnt/user/appdata/stirling-pdf/customFiles`, `/mnt/user/appdata/stirling-pdf/pipeline`, `/mnt/user/appdata/stirling-pdf/tessdata` | keine | keine separaten Secret-Dateien dokumentiert | Traefik, Authelia | UI startet, PDF-Tools verfuegbar | | ddns-updater | Rebuildbar | geringe Persistenzrelevanz | keine | Provider-Zugang ueber Stack ENV | Internetzugang | Update-Job laeuft | --- diff --git a/docs/SECRETS_MAP.md b/docs/SECRETS_MAP.md index 60bcab8..a24b846 100644 --- a/docs/SECRETS_MAP.md +++ b/docs/SECRETS_MAP.md @@ -37,6 +37,9 @@ Dieses Dokument listet sensible Daten, deren Ablageorte und die vorgesehene Einb | Komodo Core | App Secrets | Stack ENV `${KOMODO_SECRET_KEY}`, `${KOMODO_JWT_SECRET}`, `${KOMODO_MONGO_PASSWORD}`, `${KOMODO_PERIPHERY_PASSKEY}` | aktiv | | Homepage | API Tokens / Zugangsdaten | Stack ENV `HOMEPAGE_VAR_*` | aktiv | | speedtest-tracker | App Key / Admin-Zugang | Stack ENV `${APP_KEY}`, `${ADMIN_PASSWORD}` | aktiv | +| Nextcloud | Admin User | `/mnt/user/appdata/secrets/nextcloud_admin_user.txt` -> `NEXTCLOUD_ADMIN_USER_FILE` | neu | +| Nextcloud | Admin Password | `/mnt/user/appdata/secrets/nextcloud_admin_password.txt` -> `NEXTCLOUD_ADMIN_PASSWORD_FILE` | neu | +| nextcloud-postgres | DB Password | `/mnt/user/appdata/secrets/nextcloud_postgres_password.txt` -> `POSTGRES_PASSWORD_FILE` | neu | | Borg UI / Borg | Admin-Login, `SECRET_KEY`, SSH-Keys, Repo-Credentials | persistent unter `/mnt/user/appdata/borg-ui/data/` | aktiv | --- @@ -61,6 +64,9 @@ Dieses Dokument listet sensible Daten, deren Ablageorte und die vorgesehene Einb |-- immich_postgres_password.txt |-- komodo_mongo_password.txt |-- mealie_postgres_password.txt +|-- nextcloud_admin_password.txt +|-- nextcloud_admin_user.txt +|-- nextcloud_postgres_password.txt |-- postgres_password.txt |-- redis_password.txt `-- vaultwarden_admin_token.txt