412 lines
15 KiB
Markdown
412 lines
15 KiB
Markdown
# Installationsanleitung: Euro-Office DocumentServer mit Nextcloud-Anbindung
|
||
|
||
Diese Anleitung beschreibt die Installation des Euro-Office DocumentServers (Docker-Variante)
|
||
auf einem Debian-13-Server und die Anbindung an eine bestehende Nextcloud-Instanz über Apache2
|
||
als Reverse-Proxy.
|
||
|
||
Projekt: https://github.com/Euro-Office/DocumentServer
|
||
Dokumentation: https://euro-office.github.io/documentation/
|
||
|
||
---
|
||
|
||
## 1. Parameter dieser Installation
|
||
|
||
Alle im Dokument verwendeten Platzhalter sind hier zentral aufgeführt. Für eine abweichende
|
||
Installation genügt es, diese Tabelle anzupassen und die Werte in den Befehlen entsprechend
|
||
zu übernehmen.
|
||
|
||
| Platzhalter | Beschreibung | Beispielwert (diese Installation) |
|
||
|-----------------------------|------------------------------------------------------------|------------------------------------------------------|
|
||
| `<DS_HOSTNAME>` | Hostname des Euro-Office DocumentServers | `eo-ds.ndnaht.de` |
|
||
| `<NC_HOSTNAME>` | Hostname der bestehenden Nextcloud-Instanz | `cloud.ndnaht.de` |
|
||
| `<IPV4_ADDRESS>` | Öffentliche IPv4-Adresse des Servers | `135.181.90.132` |
|
||
| `<IPV6_ADDRESS>` | Öffentliche IPv6-Adresse des Servers | `2a01:4f9:c014:1ba8::1` |
|
||
| `<INSTALL_DIR>` | Arbeitsverzeichnis für Docker-Compose-Stack | `/opt/euro-office` |
|
||
| `<VHOST_DIR>` | Verzeichnis der Apache-vHost-Konfigurationsdateien | `/usr/local/apache2/conf/vhosts` |
|
||
| `<INTERNAL_PORT>` | Lokaler Port, auf den der Container gemappt wird | `8080` |
|
||
| `<DOCKER_BRIDGE_NAME>` | Fester Name der Docker-Netzwerk-Bridge | `br-euroffice` |
|
||
| `<CERT_DIR>` | Verzeichnis mit TLS-Zertifikat (hier: dehydrated) | `/var/lib/dehydrated/certs/<DS_HOSTNAME>` |
|
||
| `<NC_DOCROOT>` | Docroot-Verzeichnis der Nextcloud-Installation | `/var/www/cloud.ndnaht.de/htdocs` |
|
||
| `<NC_LOGFILE>` | Pfad der Nextcloud-Logdatei | `/var/www/cloud.ndnaht.de/logs/cloud.log` |
|
||
| `<PHP_BINARY>` | Pfad zum PHP-CLI-Binary | `/usr/local/php/bin/php` |
|
||
| `<JWT_SECRET>` | Gemeinsames Secret zwischen DocumentServer und Nextcloud | wird generiert (siehe Schritt 3) |
|
||
|
||
---
|
||
|
||
## 2. Voraussetzungen
|
||
|
||
- **Betriebssystem:** Debian 13 (getestet), amd64 oder arm64
|
||
- **Docker Engine** (Community Edition) inkl. **Compose-Plugin v2**, installiert über das offizielle Docker-Repository:
|
||
```
|
||
deb [arch=amd64 signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian trixie stable
|
||
```
|
||
- **Mindestens 4 GB RAM** (8 GB empfohlen für Mehrbenutzerbetrieb), 10 GB freier Plattenplatz
|
||
- **Bestehende Nextcloud-Instanz** (getestet mit Nextcloud 34), erreichbar über Apache2, mit SSH-/`occ`-Zugriff
|
||
- **DNS-Eintrag** für `<DS_HOSTNAME>`, zeigt auf `<IPV4_ADDRESS>` (bzw. `<IPV6_ADDRESS>`)
|
||
- **Gültiges TLS-Zertifikat** für `<DS_HOSTNAME>` (hier über [dehydrated](https://github.com/dehydrated-io/dehydrated) verwaltet)
|
||
- **Apache2** bereits installiert und aktiv, mit folgenden Modulen aktiviert:
|
||
`proxy`, `proxy_http`, `proxy_wstunnel`, `ssl`, `headers`, `rewrite`
|
||
- **Firewall-Hinweis:** Falls der Server eine eigene iptables/nftables-Firewall betreibt
|
||
(Eigenbau-Skript, CSF, ufw o. ä.), die *nicht* automatisch mit Docker zusammenarbeitet,
|
||
siehe Abschnitt 8 ("Firewall-Anpassung für Docker-Bridge-Netzwerke"). Ohne diesen Schritt
|
||
ist der Container u. U. nicht einmal vom Host selbst erreichbar.
|
||
|
||
---
|
||
|
||
## 3. Arbeitsverzeichnis und Secrets anlegen
|
||
|
||
```bash
|
||
sudo mkdir -p <INSTALL_DIR>
|
||
cd <INSTALL_DIR>
|
||
```
|
||
|
||
JWT-Secret generieren und in `.env` ablegen (wird von Docker Compose automatisch eingelesen):
|
||
|
||
```bash
|
||
echo "EO_JWT_SECRET=$(openssl rand -hex 32)" | sudo tee .env
|
||
sudo chmod 600 .env
|
||
```
|
||
|
||
> Diesen Wert später 1:1 in die Nextcloud-Konfiguration übernehmen (Schritt 10).
|
||
|
||
---
|
||
|
||
## 4. docker-compose.yaml
|
||
|
||
Der Container wird **nicht** direkt öffentlich exponiert, sondern nur auf `127.0.0.1`
|
||
gebunden — Apache übernimmt als Reverse-Proxy die öffentliche Erreichbarkeit inkl. TLS.
|
||
Das Docker-Netzwerk bekommt einen **festen Bridge-Namen**, damit Firewall-Regeln, die
|
||
auf den Interface-Namen verweisen, auch nach einem Neuaufbau des Netzwerks gültig bleiben.
|
||
|
||
```yaml
|
||
services:
|
||
euro-office:
|
||
image: ghcr.io/euro-office/documentserver:latest
|
||
container_name: euro-office-ds
|
||
restart: unless-stopped
|
||
environment:
|
||
- JWT_ENABLED=true
|
||
- JWT_SECRET=${EO_JWT_SECRET}
|
||
# EXAMPLE_ENABLED bewusst NICHT gesetzt (=false) -> kein offenes Beispiel-App in Produktion
|
||
ports:
|
||
- "127.0.0.1:<INTERNAL_PORT>:80"
|
||
volumes:
|
||
- eo_data:/var/lib/euro-office/documentserver
|
||
- eo_logs:/var/log/euro-office/documentserver
|
||
- eo_config:/etc/euro-office/documentserver
|
||
networks:
|
||
- euroffice
|
||
healthcheck:
|
||
test: ["CMD-SHELL", "curl -sf http://localhost/healthcheck || exit 1"]
|
||
interval: 30s
|
||
timeout: 10s
|
||
retries: 5
|
||
start_period: 60s
|
||
|
||
volumes:
|
||
eo_data:
|
||
eo_logs:
|
||
eo_config:
|
||
|
||
networks:
|
||
euroffice:
|
||
driver: bridge
|
||
driver_opts:
|
||
com.docker.network.bridge.name: <DOCKER_BRIDGE_NAME>
|
||
```
|
||
|
||
Datei anlegen:
|
||
|
||
```bash
|
||
cd <INSTALL_DIR>
|
||
sudo tee docker-compose.yaml > /dev/null << 'EOF'
|
||
# --- Inhalt wie oben, Platzhalter durch tatsächliche Werte ersetzt ---
|
||
EOF
|
||
```
|
||
|
||
Konfiguration prüfen, bevor gestartet wird (zeigt das aufgelöste `JWT_SECRET`):
|
||
|
||
```bash
|
||
sudo docker compose config
|
||
```
|
||
|
||
---
|
||
|
||
## 5. Container starten
|
||
|
||
```bash
|
||
cd <INSTALL_DIR>
|
||
sudo docker compose up -d
|
||
sudo docker compose ps
|
||
```
|
||
|
||
Erwartet: Status `Up ... (healthy)` nach ca. 60–90 Sekunden (erste Fontgenerierung dauert etwas).
|
||
|
||
Lokalen Health-Check testen:
|
||
|
||
```bash
|
||
curl -i --max-time 5 http://127.0.0.1:<INTERNAL_PORT>/healthcheck
|
||
```
|
||
|
||
Erwartet: `HTTP/1.1 200 OK`
|
||
|
||
> **Falls dieser Test hängt/fehlschlägt (Timeout oder Connection Reset), obwohl
|
||
> `docker compose ps` bereits `healthy` zeigt:** Das deutet auf ein Firewall-Problem hin —
|
||
> siehe Abschnitt 8.
|
||
|
||
---
|
||
|
||
## 6. Apache-Module prüfen/aktivieren
|
||
|
||
```bash
|
||
sudo a2enmod proxy proxy_http proxy_wstunnel ssl headers rewrite
|
||
sudo systemctl restart apache2
|
||
sudo systemctl status apache2 --no-pager
|
||
```
|
||
|
||
---
|
||
|
||
## 7. Apache-vHost anlegen
|
||
|
||
Datei `<VHOST_DIR>/<DS_HOSTNAME>.conf`:
|
||
|
||
```apache
|
||
<VirtualHost <IPV4_ADDRESS>:80 [<IPV6_ADDRESS>]:80>
|
||
ServerName <DS_HOSTNAME>
|
||
RewriteEngine on
|
||
RewriteCond %{HTTPS} !=on
|
||
RewriteRule (.*) https://%{SERVER_NAME}%{REQUEST_URI} [R=301,L]
|
||
CustomLog /var/log/apache2/<DS_HOSTNAME>-access.log combined
|
||
ErrorLog /var/log/apache2/<DS_HOSTNAME>-error.log
|
||
</VirtualHost>
|
||
|
||
<VirtualHost <IPV4_ADDRESS>:443 [<IPV6_ADDRESS>]:443>
|
||
ServerName <DS_HOSTNAME>
|
||
|
||
SSLEngine on
|
||
SSLCertificateFile <CERT_DIR>/fullchain.pem
|
||
SSLCertificateKeyFile <CERT_DIR>/privkey.pem
|
||
|
||
ProxyPreserveHost On
|
||
ProxyRequests Off
|
||
|
||
# WebSocket-Verbindungen (Co-Editing) an proxy_wstunnel weiterreichen
|
||
RewriteEngine On
|
||
RewriteCond %{HTTP:Upgrade} =websocket [NC]
|
||
RewriteRule ^/(.*) ws://127.0.0.1:<INTERNAL_PORT>/$1 [P,L]
|
||
RewriteCond %{HTTP:Upgrade} !=websocket [NC]
|
||
RewriteRule ^/(.*) http://127.0.0.1:<INTERNAL_PORT>/$1 [P,L]
|
||
|
||
# Wichtig: DocumentServer muss wissen, dass er hinter TLS läuft,
|
||
# sonst generiert er interne Editor-/Cache-URLs mit http:// statt https://
|
||
# ("Mixed Content", Editor kann Dokumente nicht laden/herunterladen).
|
||
RequestHeader set X-Forwarded-Proto "https"
|
||
RequestHeader set X-Forwarded-Host "<DS_HOSTNAME>"
|
||
RequestHeader set X-Forwarded-Port "443"
|
||
|
||
ProxyPass / http://127.0.0.1:<INTERNAL_PORT>/ retry=0
|
||
ProxyPassReverse / http://127.0.0.1:<INTERNAL_PORT>/
|
||
|
||
# Große Uploads (Dokumente) erlauben
|
||
LimitRequestBody 0
|
||
|
||
CustomLog /var/log/apache2/<DS_HOSTNAME>-access.log combined
|
||
ErrorLog /var/log/apache2/<DS_HOSTNAME>-error.log
|
||
</VirtualHost>
|
||
```
|
||
|
||
Aktivieren und testen:
|
||
|
||
```bash
|
||
sudo apachectl configtest
|
||
sudo systemctl reload apache2
|
||
```
|
||
|
||
Erreichbarkeit von außen prüfen:
|
||
|
||
```bash
|
||
curl -i https://<DS_HOSTNAME>/healthcheck
|
||
```
|
||
|
||
Erwartet: `HTTP/1.1 200 OK`
|
||
|
||
---
|
||
|
||
## 8. Firewall-Anpassung für Docker-Bridge-Netzwerke
|
||
|
||
**Nur relevant, wenn eine eigene Firewall-Lösung eingesetzt wird, die den Datenverkehr über
|
||
Docker-Bridge-Interfaces nicht automatisch zulässt** (z. B. eigene iptables/nftables-Skripte
|
||
mit einer restriktiven `OUTPUT`/`FORWARD`-Policy).
|
||
|
||
### Symptom
|
||
|
||
- `docker compose ps` zeigt den Container als `healthy` (interner Check funktioniert)
|
||
- Ein `curl` **von außerhalb des Containers** auf `127.0.0.1:<INTERNAL_PORT>` oder die
|
||
Container-IP hängt oder liefert `Connection reset by peer` / `Operation timed out`
|
||
- `ping` auf die Container-IP funktioniert (ICMP ist oft separat erlaubt), TCP jedoch nicht
|
||
|
||
### Ursache
|
||
|
||
Docker bindet Container-Ports über einen `docker-proxy`-Prozess bzw. NAT-Regeln, die
|
||
Datenverkehr über das jeweilige Docker-Bridge-Interface (`br-...`) leiten. Eine restriktive
|
||
Firewall mit interface-spezifischen `ACCEPT`-Regeln (z. B. nur für `eth0`) und einer
|
||
abschließenden `DROP`-Regel lässt diesen Bridge-Verkehr nicht automatisch durch.
|
||
|
||
### Lösung
|
||
|
||
1. Docker-Netzwerk mit **festem Bridge-Namen** betreiben (siehe Abschnitt 4,
|
||
`com.docker.network.bridge.name: <DOCKER_BRIDGE_NAME>`) — verhindert, dass sich der
|
||
Interface-Name bei jedem Neuaufbau des Netzwerks ändert.
|
||
2. In der Firewall-Konfiguration eine **explizite Ausnahme für dieses eine Interface**
|
||
eintragen (kein Wildcard wie `br+`, das würde auch fremde/zukünftige Bridges ungefiltert
|
||
durchlassen). Je nach Firewall-Lösung z. B.:
|
||
- Eigenbau-Skript mit `unprotected_ifs`-Variable: `unprotected_ifs="<DOCKER_BRIDGE_NAME>"`
|
||
- iptables direkt: gezielte `ACCEPT`-Regeln für `-i <DOCKER_BRIDGE_NAME>` / `-o <DOCKER_BRIDGE_NAME>`
|
||
in den Chains `INPUT`, `OUTPUT` und `FORWARD`
|
||
3. Firewall neu laden und Docker-Daemon **danach** neu starten, damit Docker seine eigenen
|
||
iptables-Chains (`DOCKER`, `DOCKER-FORWARD`, `DOCKER-USER`, …) neu aufbaut:
|
||
```bash
|
||
sudo systemctl restart <firewall-service>
|
||
sudo systemctl restart docker
|
||
cd <INSTALL_DIR> && sudo docker compose up -d
|
||
```
|
||
4. **Startreihenfolge dauerhaft absichern**, damit das Problem nach einem Server-Reboot nicht
|
||
erneut auftritt — Docker soll immer *nach* der Firewall starten:
|
||
```bash
|
||
sudo mkdir -p /etc/systemd/system/docker.service.d/
|
||
sudo tee /etc/systemd/system/docker.service.d/after-firewall.conf > /dev/null << 'EOF'
|
||
[Unit]
|
||
After=<firewall-service>.service
|
||
Wants=<firewall-service>.service
|
||
EOF
|
||
sudo systemctl daemon-reload
|
||
```
|
||
|
||
### Restrisiko
|
||
|
||
Ein manueller Neustart der Firewall **während Docker bereits läuft** stört bestehende,
|
||
schon aktive Container-Verbindungen in der Regel nicht. Wird jedoch **danach** ein neues
|
||
Docker-Netzwerk angelegt (z. B. `docker compose down && up`, oder ein neuer Compose-Stack),
|
||
kann die Regel-Einfügung fehlschlagen (`Failed to Setup IP tables: ... No chain/target/match
|
||
by that name`), weil Dockers eigene Chains beim Firewall-Neuaufbau entfernt wurden. In diesem
|
||
Fall genügt es, den Docker-Daemon einmal neu zu starten und den Stack erneut zu starten:
|
||
|
||
```bash
|
||
sudo systemctl restart docker
|
||
cd <INSTALL_DIR> && sudo docker compose up -d
|
||
```
|
||
|
||
### Verifikation nach einem vollständigen Server-Reboot
|
||
|
||
```bash
|
||
sudo reboot
|
||
# nach dem Reboot, neu verbinden:
|
||
systemctl status <firewall-service> --no-pager
|
||
systemctl status docker.service --no-pager
|
||
sudo docker compose -f <INSTALL_DIR>/docker-compose.yaml ps
|
||
curl -i --max-time 5 https://<DS_HOSTNAME>/healthcheck
|
||
ip a | grep -A2 <DOCKER_BRIDGE_NAME>
|
||
sudo docker network ls
|
||
```
|
||
|
||
---
|
||
|
||
## 9. Nextcloud-Connector-App installieren
|
||
|
||
Die Euro-Office-Integration ist im offiziellen Nextcloud App Store gelistet.
|
||
|
||
**Über die Weboberfläche:**
|
||
|
||
1. Als Administrator einloggen
|
||
2. Profilbild (oben rechts) → **Apps**
|
||
3. Kategorie **„Büro & Text"** auswählen
|
||
4. **„Euro-Office"** suchen → **„Download and enable"**
|
||
|
||
**Alternativ über die Konsole (`occ`):**
|
||
|
||
```bash
|
||
cd <NC_DOCROOT>
|
||
sudo -u www-data <PHP_BINARY> occ app:install eurooffice
|
||
```
|
||
|
||
`app:install` lädt die App aus dem Nextcloud App Store herunter **und** aktiviert sie in
|
||
einem Schritt. Falls die App bereits heruntergeladen, aber nur deaktiviert ist, genügt
|
||
stattdessen:
|
||
|
||
```bash
|
||
sudo -u www-data <PHP_BINARY> occ app:enable eurooffice
|
||
```
|
||
|
||
**Kontrolle per SSH:**
|
||
|
||
```bash
|
||
sudo -u www-data <PHP_BINARY> <NC_DOCROOT>/occ app:list | grep -i eurooffice
|
||
```
|
||
|
||
Erwartet: Eintrag unter *Enabled*, z. B. `eurooffice: 11.0.1`
|
||
|
||
---
|
||
|
||
## 10. Nextcloud konfigurieren (DocumentServer-URL + JWT-Secret)
|
||
|
||
Zuerst prüfen, welche Config-Keys die App verwendet:
|
||
|
||
```bash
|
||
cd <NC_DOCROOT>
|
||
sudo -u www-data <PHP_BINARY> occ config:list eurooffice
|
||
```
|
||
|
||
DocumentServer-URL setzen:
|
||
|
||
```bash
|
||
sudo -u www-data <PHP_BINARY> occ config:app:set eurooffice DocumentServerUrl --value="https://<DS_HOSTNAME>/"
|
||
```
|
||
|
||
JWT-Secret setzen (identischer Wert wie in `<INSTALL_DIR>/.env`, Variable `EO_JWT_SECRET`):
|
||
|
||
```bash
|
||
sudo grep EO_JWT_SECRET <INSTALL_DIR>/.env
|
||
sudo -u www-data <PHP_BINARY> occ config:app:set eurooffice jwt_secret --value="<JWT_SECRET>"
|
||
```
|
||
|
||
Kontrolle:
|
||
|
||
```bash
|
||
sudo -u www-data <PHP_BINARY> occ config:list eurooffice
|
||
```
|
||
|
||
---
|
||
|
||
## 11. Funktionstest
|
||
|
||
1. In Nextcloud ein Office-Dokument (`.docx`, `.xlsx`, `.pptx`) öffnen
|
||
2. Der Euro-Office-Editor sollte sich im Browser öffnen und das Dokument anzeigen
|
||
|
||
**Falls die Meldung „Herunterladen ist fehlgeschlagen" erscheint:**
|
||
|
||
Browser-Entwicklerkonsole (F12) prüfen. Eine Meldung wie
|
||
*„Laden von gemischten aktiven Inhalten … wurde blockiert"* (Mixed Content) weist darauf hin,
|
||
dass der DocumentServer interne URLs mit `http://` statt `https://` erzeugt. Prüfen, ob die
|
||
`X-Forwarded-*`-Header im vHost (Abschnitt 7) korrekt gesetzt sind, danach:
|
||
|
||
```bash
|
||
sudo apachectl configtest
|
||
sudo systemctl reload apache2
|
||
```
|
||
|
||
Browser-Hard-Reload (`Strg+Shift+R`) und Dokument erneut öffnen.
|
||
|
||
---
|
||
|
||
## 12. Wartung / Hinweise
|
||
|
||
- **Volumes:** `eo_data`, `eo_logs`, `eo_config` (benannte Docker-Volumes) enthalten
|
||
Konfiguration, Cache und Logs. Für Backups: `docker run --rm -v <volume>:/data -v $(pwd):/backup alpine tar czf /backup/<volume>.tar.gz -C /data .`
|
||
- **Updates:** `docker compose pull && docker compose up -d`
|
||
- **Logs im Container:**
|
||
```bash
|
||
sudo docker compose exec euro-office find /var/log/euro-office -iname "*.log"
|
||
```
|
||
- **EXAMPLE_ENABLED:** bewusst nicht gesetzt/aktiviert — die Beispiel-App hat keine
|
||
Zugriffskontrolle und darf nicht öffentlich erreichbar sein.
|