Go-Backend (chi, pgx, golang-migrate) + React/TS-Frontend als PWA. Enthalten: Auth inkl. OIDC, Dateien mit Versionen und Papierkorb, Freigaben (Links + intern), WebDAV, Gruppenordner, Admin/Theming, Volltextsuche mit OCR, eigener PDF-Betrachter und WOPI-Office (Collabora/OnlyOffice). Nicht im Repo: deploy/.env (Sitzungsschlüssel, DB- und Admin-Passwort) sowie alle Daten- und Build-Verzeichnisse – siehe .gitignore. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
16 KiB
Aether
Self-hosted cloud storage — schnelle, schlanke Alternative zu Nextcloud.
Stack: Go + chi · React + TS + Vite · PostgreSQL · Docker Compose
Reverse Proxy (Pflicht)
Aether lauscht intern auf reinem HTTP. TLS wird von deinem eigenen Reverse Proxy terminiert. nginx, Traefik, Cloudflare Tunnel – alles funktioniert. Dieses Projekt liefert bewusst keinen Proxy mit: TLS, Zertifikate und Domain gehören in deine Hand, nicht in ein Compose-File, das du ohnehin überschreiben müsstest.
Der Proxy muss folgende Headers setzen:
X-Forwarded-For: <echte Client-IP>
X-Forwarded-Proto: https
Die IP deines Proxys muss in TRUSTED_PROXIES stehen – sonst ignoriert Aether diese Header
(Sicherheitsmaßnahme gegen IP-Spoofing).
Der Proxy muss folgendes durchreichen (keine eigene Verarbeitung):
- Alle HTTP-Methoden inkl.
PROPFIND,MKCOL,MOVE,COPY(für WebDAV) - Große Uploads (Timeout und Body-Size-Limit großzügig setzen)
- WebSocket-Verbindungen (für Office-Co-Editing, Phase 6)
- HSTS-Header setzen (z. B.
Strict-Transport-Security: max-age=31536000; includeSubDomains)
Schnellstart (lokal)
Voraussetzungen
- Docker + Docker Compose
- Go 1.25+ und Node 20+ nur, wenn du aus dem Quelltext bauen willst – zum Betreiben genügt das fertige Abbild (siehe Schritt 3)
1. Repository klonen
git clone <repo-url> aether
cd aether
2. .env anlegen
cp deploy/.env.example deploy/.env
Dann in deploy/.env ausfüllen:
SESSION_SECRET=$(openssl rand -hex 32)
DB_PASSWORD=<sicheres_passwort>
PUBLIC_URL=https://cloud.example.com
TRUSTED_PROXIES=<IP-deines-Proxys>
ADMIN_USERNAME=admin
ADMIN_PASSWORD=<sicheres_admin_passwort>
3. Starten
cd deploy
docker login gitea.honeyy.duckdns.org # nur beim ersten Mal
docker compose up -d
Das Compose zieht ein fertiges Abbild aus der eigenen Registry
(gitea.honeyy.duckdns.org/honey/aether). Für eine neue Umgebung reichen damit
drei Dateien – docker-compose.yml, .env und das Datenverzeichnis. Backend
und Frontend müssen dort nicht liegen und nicht gebaut werden.
Wer aus dem Quelltext bauen will, kommentiert in docker-compose.yml image:
aus und den build:-Block ein, dann:
docker compose build app && docker compose up -d
Aktualisieren:
docker compose pull app && docker compose up -d app
Beim ersten Start:
- Postgres-Migrations laufen automatisch
- Der erste Admin-Account wird aus
ADMIN_USERNAME/ADMIN_PASSWORDangelegt - Nach dem Start:
ADMIN_PASSWORDaus.enventfernen oder leer lassen
4. Öffnen
Die App ist über deinen Reverse Proxy erreichbar (z. B. https://cloud.example.com).
Direkt (ohne Proxy): http://localhost:8080/health
Abbild bauen und veröffentlichen
# im Wurzelverzeichnis des Projekts, nicht in deploy/
IMG=gitea.honeyy.duckdns.org/honey/aether
docker build -f deploy/Dockerfile -t $IMG:$(date +%Y.%m.%d) -t $IMG:latest .
docker push $IMG:$(date +%Y.%m.%d)
docker push $IMG:latest
Zwei Tags, mit Absicht. latest ist der bewegliche Name zum Aufbauen, der
datierte der feste zum Zurückrollen. Wer nur latest fährt, hat im Fehlerfall
nichts, worauf er zurück kann – der Name zeigt dann schon auf die Fassung, die
gerade Ärger macht.
Das Abbild wiegt rund 540 MB. Den Löwenanteil machen ocrmypdf,
tesseract samt Sprachmodellen und poppler-utils aus – ohne sie wären es
etwa 210 MB, aber auch keine Volltextsuche in Scans. Wer das nicht braucht,
streicht die zweite apk-Zeile im Dockerfile und setzt OCR_ENABLED=false;
der Server prüft beim Start selbst, ob die Programme da sind, und läuft sonst
ohne Volltextsuche weiter.
Gebaut wird für die Architektur des Wirts. Für ein anderes Ziel (etwa ein
ARM-Gerät) braucht es docker buildx build --platform linux/arm64.
Lokale Entwicklung
Backend (immer via Docker)
Das Backend wird ausschließlich über Docker Compose gestartet – nie mit go run.
Postgres-Migrations und DB-Verbindung sind auf die Compose-Umgebung ausgelegt.
cd deploy
docker compose up -d --build # Backend + Postgres starten
docker compose logs -f app # Logs verfolgen
docker compose down # stoppen
Code-Änderungen am Backend: docker compose up -d --build neu ausführen (Image neu bauen).
Compile-Check ohne lokales Go-Toolchain (der Host hat kein Go installiert):
docker run --rm -v "$PWD/../backend":/src -w /src golang:1.25-alpine go build ./...
Frontend
cd frontend
npm install
npm run dev # http://localhost:5173 (Vite dev server, spricht Backend auf :8080 an)
npm run build # Production-Build (wird vom Backend-Image mitgebaut)
Tests
# Backend (im Go-Image, da Host kein Go hat)
docker run --rm -v "$PWD/backend":/src -w /src golang:1.25-alpine go test ./...
# Frontend
cd frontend && npm test
Struktur
aether/
├── backend/
│ ├── cmd/aether/ # Einstiegspunkt
│ ├── internal/ # Domänen-Pakete (auth, storage, files, …)
│ ├── db/migrations/ # SQL-Migrations
│ └── pkg/ # Shared utils (config, middleware, response)
├── frontend/
│ └── src/
│ ├── api/ # Typed API-Client
│ ├── components/
│ ├── hooks/
│ └── pages/
├── deploy/
│ ├── docker-compose.yml # app + postgres (kein Proxy-Container)
│ ├── Dockerfile
│ └── .env.example
└── CLAUDE.md # Projektkonventionen
Wo die Dateien liegen
Nicht in einem Docker-Volume, sondern in einem gewöhnlichen Verzeichnis auf dem
Wirt – DATA_PATH in der .env. Der Aufbau darin ist derselbe wie in der
Oberfläche:
<DATA_PATH>/
├── users/<benutzername>/home/… ← die eigenen Dateien, echte Namen
│ /.trash/… ← Papierkorb
│ /.versions/… ← frühere Fassungen
├── groups/<gruppen-id>/home/… ← Gruppenordner
├── thumbs/ ← Vorschaubilder
├── branding/ ← Logo und App-Symbol
└── tmp/ ← Zwischendateien, wird selbst geräumt
Warum das so ist: Ein Archiv ist nur dann eines, wenn man ohne die
Anwendung an seine Unterlagen kommt. Hier reicht dafür scp -r oder ein Blick
in den Dateimanager – keine Datenbank, kein Export, kein laufender Container
nötig. Bei einem benannten Volume läge alles unter
/var/lib/docker/volumes/… und gehörte root.
Damit die Dateien dir gehören und nicht einem Container-Nutzer, läuft der
Container unter deiner Kennung (APP_UID/APP_GID, siehe .env). Damit
funktioniert SFTP direkt:
sftp dein-nutzer@dein-server
get -r /pfad/zu/aether-data/users/deinname/home
Nur lesen. Wer von Hand eine Datei hineinlegt, hat sie auf der Platte, aber nicht in der Datenbank: Die Cloud sieht sie nicht, und die Speicherbereinigung räumt sie irgendwann weg. Zum Hineinlegen ist WebDAV der Weg (siehe unten).
Von einem benannten Volume umsteigen
Ältere Installationen hatten ein Volume deploy_data. Umzug mit Prüfung:
cd deploy
docker compose stop app
mkdir -p /pfad/zu/aether-data
docker run --rm -v deploy_data:/src:ro -v /pfad/zu/aether-data:/dst alpine:3.20 \
sh -c 'cp -a /src/. /dst/ && chown -R $(id -u):$(id -g) /dst'
# Prüfen, bevor umgeschaltet wird: beide Summen müssen gleich sein
docker run --rm -v deploy_data:/src:ro alpine:3.20 \
sh -c 'cd /src && find . -type f -exec sha256sum {} \; | LC_ALL=C sort | sha256sum'
(cd /pfad/zu/aether-data && find . -type f -exec sha256sum {} \; | LC_ALL=C sort | sha256sum)
# erst dann DATA_PATH in die .env eintragen und starten
docker compose up -d app
Das alte Volume nicht sofort löschen. Es ist der Rückfall, solange nicht
alles geprüft ist; docker volume rm deploy_data kann warten, bis du sicher
bist.
WebDAV
Adresse: https://<deine-domain>/dav/
Angemeldet wird sich mit dem Benutzernamen und einem App-Passwort
(Profil → App passwords). Das Kontopasswort wird an /dav/ abgelehnt – es
ist zugleich der Admin-Zugang und hat auf Geräten nichts verloren. Ein
App-Passwort gilt nur für WebDAV, nie fürs Web-Login, und lässt sich einzeln
widerrufen, ohne die anderen Geräte auszusperren.
Der Aufbau entspricht dem der Weboberfläche: eigene Ordner und Gruppenordner
liegen nebeneinander in der Wurzel, Freigaben von Personen unter
/dav/Shared/<Person>/<Freigabe>/. Trägt ein Gruppenordner denselben Namen wie
ein eigener, behält der eigene ihn und der Gruppenordner bekommt den
Gruppennamen angehängt (Projekt (Buero)).
Nach zehn Fehlanmeldungen in 15 Minuten sperrt die IP für den Rest des Fensters
(429). Normale Anfragen zählen nicht mit – Sync-Clients senden legitim
hunderte pro Minute.
Office (Collabora / OnlyOffice)
Aether ist WOPI-Host. Das ist die Schnittstelle, über die Office-Server
sich eine Datei holen und wieder zurückschreiben – Collabora und OnlyOffice
sprechen beide dieselbe. Deshalb wird hier ein Adapter gebaut und der
Anbieter per .env umgeschaltet, statt zweimal dasselbe zu integrieren.
Was dazukommt
OFFICE_PROVIDER |
collabora, onlyoffice oder leer (aus) |
OFFICE_URL |
wo das Backend den Office-Server erreicht |
OFFICE_PUBLIC_URL |
wo der Browser ihn erreicht (leer = wie oben) |
OFFICE_WOPI_URL |
wo der Office-Server Aether erreicht (leer = PUBLIC_URL) |
OFFICE_ALLOWED_HOSTS |
Ziele, die das Backend anfragen darf – gegen SSRF |
Drei Adressen für drei Richtungen. Das wirkt übertrieben, bis man es einmal
aufgebaut hat: Im Compose-Netz heißt der Office-Server collabora, im Browser
muss dort ein auflösbarer Name stehen, und der Office-Server wiederum erreicht
Aether unter app:8080 – „localhost" ist aus seinem Container heraus er selbst.
Der Container startet nur mit dem Profil, sonst kostet er Arbeitsspeicher, ohne gebraucht zu werden:
docker compose --profile office up -d
Welche Dateien, und wie gut
Bearbeitet wird nur, was auf der Positivliste steht:
odt docx doc rtf txt · ods xlsx xls csv · odp pptx ppt · odg.
PDF steht bewusst nicht darauf – es hat seinen eigenen Betrachter, und ein
Office-Server würde beim Speichern ein anderes Dokument daraus machen.
Collabora Online ist LibreOffice, mit derselben Datei-Unterstützung. Für die Microsoft-Formate heißt das:
docx, xlsx, pptx |
öffnen und speichern zuverlässig, Format bleibt erhalten |
Alte doc, xls, ppt |
gehen, sind aber Rückwärtskompatibilität – kein Ziel der Entwicklung |
| Komplexes Layout | Seitenumbrüche und Schriftabstände können um Kleinigkeiten abweichen |
| Sehr große Tabellen, Pivot, seltene Formeln | die häufigste Stelle für echte Abweichungen |
| Makros (VBA) | laufen nicht – und das ist Absicht, nicht Mangel |
Fehlende Schriften sind der häufigste Grund für „sieht anders aus": Der Container bringt Calibri und Cambria nicht mit. Wer Dokumente von außen bearbeitet, legt die Schriften nach.
Für ein Belegarchiv ist das unkritisch – da wird gelesen und selten geändert. Für den täglichen Umgang mit fremden Word-Dateien gilt: Rundlauf prüfen, nicht darauf vertrauen.
Wie das zusammenspielt
Der Office-Server ist kein Client deines Browsers allein – er holt die Datei selbst beim Backend ab:
Browser ──────────► Office-Server (Bedienoberfläche im <iframe>)
Office-Server ────► Aether /wopi/... (Datei holen, Datei zurückschreiben)
Beide Pfeile müssen funktionieren. Das ist der Punkt, an dem solche
Installationen scheitern: Der Office-Server muss Aether unter seiner
öffentlichen Adresse erreichen (PUBLIC_URL), nicht nur unter
localhost. Ein Zugriffstoken mit kurzer Laufzeit hängt an jedem Aufruf; ohne
gültiges Token liefert der WOPI-Endpunkt nichts.
Was beim Aufbau schiefgeht
Alles hier ist beim Testen tatsächlich passiert, in dieser Reihenfolge:
| Symptom | Ursache |
|---|---|
| Rahmen bleibt leer, Konsole: „Framing … violates default-src 'self'" | Unsere eigene Sicherheitsrichtlinie. frame-src fällt auf default-src zurück; der Office-Ursprung muss ausdrücklich erlaubt sein. Passiert automatisch, sobald OFFICE_PROVIDER gesetzt ist. |
| Editor lädt, Dokument bleibt weiß | ssl.termination=true ohne HTTPS davor. Collabora sagt dem Browser dann wss://, das es nicht gibt. Im HTTP-Test OFFICE_SSL_TERMINATION=false. |
| Log: „CheckFileInfo returned 0 … ECONNREFUSED" | OFFICE_WOPI_URL zeigt auf 127.0.0.1:8090. Der Port ist auf loopback gebunden – aus einem Container nicht erreichbar. Im Compose den Dienstnamen nehmen. |
| Log: „WOPISrc must be URI-encoded" | Behoben; die Adresse wird vollständig kodiert. |
| Log: „Failed to exec coolmount … needs CAP_SYS_ADMIN" | Gelöst über --o:mount_jail_tree=false im Compose. |
Fehlende Symbole (404 auf .svg) |
Lücken im Collabora-Image selbst. Kosmetisch. |
Sicherheit
- SSRF: Das Backend ruft den Office-Server auf. Ausgehende Ziele werden auf
OFFICE_ALLOWED_HOSTSbeschränkt – sonst wird aus einer Adresse in der Konfiguration ein Weg ins interne Netz. - Tokens sind dateibezogen und kurzlebig. Ein Token für Datei A öffnet Datei B nicht, auch nicht mit derselben Sitzung.
- Schreiben folgt derselben Regel wie ein Upload: atomar schreiben, SHA-256 neu berechnen, Version anlegen, wenn Versionierung an ist. Ein Office-Server ist kein Sonderweg an der Datenintegrität vorbei.
Speicherbedarf – vor der Entscheidung lesen
Der Office-Server ist der mit Abstand hungrigste Teil dieser Anlage:
| Bedarf | Bemerkung | |
|---|---|---|
| Collabora (CODE) | ~1 GB im Leerlauf, +100–200 MB je offenem Dokument | LibreOffice-Kern, startet pro Dokument einen Prozess |
| OnlyOffice Docs | 2 GB Minimum, 4 GB empfohlen, dazu Swap | bringt eigenes Postgres, RabbitMQ und Node mit |
Dazu kommt, was schon läuft: Postgres, Aether und – nicht unterschätzen – die Texterkennung. ocrmypdf und ghostscript nehmen sich bei einem großen Scan kurzzeitig mehrere hundert Megabyte.
Welchen nehmen?
- Collabora, wenn der Speicher knapp ist oder Herkunft und ODF zählen. LibreOffice-Kern, europäisch, deutlich genügsamer.
- OnlyOffice, wenn
.docx/.xlsxzwischen Desktop-Office und Web hin und her wandern. Beste Treue bei OOXML, weil es nativ damit arbeitet. - EuroOffice ist ein junger OnlyOffice-Fork und nutzt denselben
WOPI-Weg. Eine Entscheidung für OnlyOffice heute verbaut ihn nicht: Es wäre
ein Wechsel des Containers und eine Zeile in der
.env.
Phasen-Roadmap
| Phase | Inhalt | Status |
|---|---|---|
| 0 | Fundament (Monorepo, Docker, Migrations, Backend-Skeleton) | ✅ |
| 1 | Auth (Argon2id, Sessions), Storage (atomar + SHA-256), Datei-CRUD, Frontend | ✅ |
| 2 | Uploads mit Fortschritt, Papierkorb, Versionen, Share-Links, Thumbnails | ✅ |
| 2.5 | Sidebar-Favoriten, Ordner verschieben/hochladen, „Verschieben nach…" | ✅ |
| 3 | Admin-Panel, Gruppen(-mitgliedschaft), Theming, User-Profil | ✅ |
| 4 | WebDAV (/dav/), OIDC (optional) |
✅ (im Test) |
| 4.5 | Härtung (Quota, Audit-Log, App-Passwörter), interne Shares, Gruppenordner, Logo/App-Symbol | ✅ (im Test) |
| 5 | UI (Suche, Vorschau, Sortierung, Vor/Zurück) – Design-Rest offen | 🔄 |
| 6 | Dokumente: OCR + Volltextsuche ✅, Etiketten ✅, Absender ✅, PDF-Betrachter ✅ – Inbox vertagt | 🔄 |
| 7 | Office (WOPI-Host, Collabora / OnlyOffice) – Anleitung | ✅ (im Test) |
| 8 | Härtung, 2FA, Observability, Extension-Points | 🔲 |
Manuelle Test-Checkliste für die fertigen Phasen: siehe
TESTING.md.