diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..1c7b13d --- /dev/null +++ b/.env.example @@ -0,0 +1,66 @@ +# Aether – das Nötigste für eine neue Umgebung. +# +# cp .env.example .env +# +# Vollständige Liste aller Schalter (OIDC, OCR, Thumbnails, Kontingente …): +# deploy/.env.example + +# ── Pflicht ─────────────────────────────────────────────────────────────────── + +# Wie die Cloud von außen erreichbar ist. Wird für OIDC-Rücksprünge, +# WOPI-Rückrufe und Freigabe-Links benutzt – ein falscher Wert fällt erst +# dort auf, nicht beim Start. +PUBLIC_URL=https://cloud.example.com + +# openssl rand -hex 32 +SESSION_SECRET= + +DB_NAME=aether +DB_USER=aether +DB_PASSWORD= + +# Wohin die Dateien auf dem Wirt kommen. Ein gewöhnliches Verzeichnis, kein +# Docker-Volume – so kommst du per SFTP oder Dateimanager daran. +DATA_PATH=/pfad/zu/aether-data + +# `id -u` und `id -g`. Ohne das gehören die Dateien einem Container-Nutzer. +APP_UID=1000 +APP_GID=1000 + +# ── Reverse Proxy ───────────────────────────────────────────────────────────── + +# X-Forwarded-For/-Proto werden NUR von diesen Adressen geglaubt. Steht hier +# das Falsche, sieht die Anwendung überall die IP des Proxys – Rate-Limit und +# Audit-Log werden damit wertlos. +TRUSTED_PROXIES=172.18.0.1 + +# Port auf dem Wirt, gebunden auf loopback. Im Container bleibt es 8080. +APP_PORT=8080 + +# ── Erster Start ────────────────────────────────────────────────────────────── + +# Legt beim allerersten Start den Admin an. Danach hier leeren – das Konto +# bleibt, das Passwort steht sonst dauerhaft in der Datei. +ADMIN_USERNAME=admin +ADMIN_PASSWORD= + +# ── Office (optional) ───────────────────────────────────────────────────────── +# Leer lassen = aus. Sonst `docker compose --profile office up -d`. +# +# Drei Adressen für drei Richtungen, und sie sind selten gleich: +# OFFICE_URL Backend → Office-Server +# OFFICE_PUBLIC_URL Browser → Office-Server +# OFFICE_WOPI_URL Office-Server → Aether (NICHT localhost – aus seinem +# Container heraus ist das er selbst) +OFFICE_PROVIDER= +OFFICE_URL=http://collabora:9980 +OFFICE_PUBLIC_URL=https://office.example.com +OFFICE_WOPI_URL=http://app:8080 +# Ziele, die das Backend anfragen darf – gegen SSRF. +OFFICE_ALLOWED_HOSTS=collabora +OFFICE_ALIASGROUP=http://app:8080 + +# Hinter deinem Reverse Proxy mit HTTPS: beide auf true. +OFFICE_SSL=false +OFFICE_SSL_TERMINATION=true +OFFICE_PORT=9980 diff --git a/.gitignore b/.gitignore index ccf0641..58cc639 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,9 @@ deploy/.env .env +# Aus den Beispielen kopiert – gehört in die Umgebung, nicht ins Repo. +/docker-compose.yml + # Go backend/aether backend/*.test diff --git a/README.md b/README.md index 51eedbe..6ebc940 100644 --- a/README.md +++ b/README.md @@ -37,52 +37,73 @@ Der Proxy muss folgendes durchreichen (keine eigene Verarbeitung): - 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 +Zwei Wege, je nachdem was du vorhast: + +| | | +|---|---| +| **Betreiben** | `docker-compose.example.yml` + `.env.example` im Wurzelverzeichnis. Zieht das fertige Abbild, braucht keinen Quelltext. | +| **Entwickeln** | `deploy/docker-compose.yml` + `deploy/.env.example`. Baut aus dem Quelltext, kennt alle Schalter. | + +### 1. Dateien holen + +Zum Betreiben genügen zwei Dateien – kein Klonen nötig: + +```bash +mkdir aether && cd aether +curl -O https://gitea.honeyy.duckdns.org/Docker/Aether/raw/branch/main/docker-compose.example.yml +curl -O https://gitea.honeyy.duckdns.org/Docker/Aether/raw/branch/main/.env.example +mv docker-compose.example.yml docker-compose.yml +``` + +Zum Entwickeln stattdessen das Repository: ```bash git clone https://gitea.honeyy.duckdns.org/Docker/Aether.git aether cd aether ``` -Zum **Betreiben** braucht es das Repository nicht – dafür genügen -`deploy/docker-compose.yml`, eine `.env` und ein Datenverzeichnis. Klonen ist -nur nötig, wenn du am Quelltext arbeiten oder selbst bauen willst. - ### 2. `.env` anlegen ```bash -cp deploy/.env.example deploy/.env +cp .env.example .env # beim Betreiben +cp deploy/.env.example deploy/.env # beim Entwickeln ``` -Dann in `deploy/.env` ausfüllen: +Ausfüllen musst du mindestens: ```env -SESSION_SECRET=$(openssl rand -hex 32) -DB_PASSWORD= PUBLIC_URL=https://cloud.example.com +SESSION_SECRET= # openssl rand -hex 32 +DB_PASSWORD= +DATA_PATH=/pfad/zu/aether-data +APP_UID=1000 # id -u +APP_GID=1000 # id -g TRUSTED_PROXIES= ADMIN_USERNAME=admin -ADMIN_PASSWORD= +ADMIN_PASSWORD= # nach dem ersten Start wieder leeren ``` +Die schlanke `.env.example` im Wurzelverzeichnis führt nur diese Pflichtwerte +und Office. Alles Weitere – OIDC, OCR, Vorschaubilder, Kontingente – steht mit +Erklärung in `deploy/.env.example`. + ### 3. Starten ```bash -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/docker/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. +Beim ersten Start: +- Postgres-Migrations laufen automatisch +- Der erste Admin-Account wird aus `ADMIN_USERNAME` / `ADMIN_PASSWORD` angelegt +- Nach dem Start: `ADMIN_PASSWORD` aus `.env` entfernen oder leer lassen -Wer aus dem Quelltext bauen will, kommentiert in `docker-compose.yml` `image:` -aus und den `build:`-Block ein, dann: +Aus dem Quelltext bauen statt ziehen: ```bash -docker compose build app && docker compose up -d +docker compose -f deploy/docker-compose.yml build app +docker compose -f deploy/docker-compose.yml up -d ``` Aktualisieren: @@ -91,11 +112,6 @@ 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_PASSWORD` angelegt -- Nach dem Start: `ADMIN_PASSWORD` aus `.env` entfernen oder leer lassen - ### 4. Öffnen Die App ist über deinen Reverse Proxy erreichbar (z. B. `https://cloud.example.com`). diff --git a/deploy/docker-compose.yml b/deploy/docker-compose.yml index a8240b4..7e102f9 100644 --- a/deploy/docker-compose.yml +++ b/deploy/docker-compose.yml @@ -1,59 +1,40 @@ +# Aether – Entwicklung: aus dem Quelltext bauen. +# +# docker compose -f deploy/docker-compose.yml build app +# docker compose -f deploy/docker-compose.yml up -d +# +# Zum **Betreiben** ist docker-compose.example.yml im Wurzelverzeichnis +# gedacht: schlanker und ohne Quelltext, es zieht das fertige Abbild. +# +# Erklärungen zu allem hier: README.md + services: app: container_name: aether-app - # Fertiges Abbild aus der eigenen Gitea-Registry. - # - # Ein Tag mit Datum **und** `latest`: Zum Aufbauen nimmt man `latest`, zum - # Zurückrollen den datierten. 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. - # - # Anmelden vor dem ersten Zug: - # docker login gitea.honeyy.duckdns.org - # - # Aktualisieren: - # docker compose pull app && docker compose up -d app + build: + # Kontext ist das Wurzelverzeichnis – das Abbild braucht backend/ und + # frontend/, nicht nur diesen Ordner. + context: .. + dockerfile: deploy/Dockerfile image: gitea.honeyy.duckdns.org/docker/aether:latest - # Selbst bauen statt ziehen? Dann die Zeile darüber auskommentieren und - # diese drei einkommentieren. Der Kontext ist das **Wurzelverzeichnis** des - # Projekts, nicht dieser Ordner – das Abbild braucht backend/ und frontend/. - # - # docker compose build app - # - # build: - # context: .. - # dockerfile: deploy/Dockerfile restart: unless-stopped env_file: .env - # Als der Nutzer laufen, dem das Datenverzeichnis auf dem Wirt gehört. - # Ohne das legt der Container seine Dateien als uid 100 an, und du kämst - # per SFTP nur als root daran. user: "${APP_UID:-1000}:${APP_GID:-1000}" - # ocrmypdf und ghostscript legen Arbeitsdateien unter $HOME ab. Bei einem - # abweichenden Nutzer ist das Heimverzeichnis aus dem Image nicht mehr - # beschreibbar – /tmp im Container ist es immer. environment: + # ocrmypdf legt Arbeitsdateien unter $HOME ab – das aus dem Image ist + # für einen abweichenden Nutzer nicht beschreibbar. HOME: /tmp depends_on: postgres: condition: service_healthy volumes: - # Bind-Mount statt eines benannten Volumes: Deine Dateien liegen damit in - # einem gewöhnlichen Verzeichnis auf dem Wirt, mit echten Ordnern und - # echten Namen (users//home/…). Du kommst per SFTP, rsync oder - # einfach im Dateimanager daran – auch dann noch, wenn diese Anwendung - # nie wieder startet. Bei einem benannten Volume läge alles unter - # /var/lib/docker/volumes/ und gehörte root. - # - # Nur lesen. Wer hier hineinschreibt, hat eine Datei auf der Platte, die - # in der Datenbank nicht existiert – die Cloud sieht sie nicht, und der - # GC räumt sie irgendwann weg. Zum Hineinlegen ist WebDAV der Weg. + # Bind-Mount, kein benanntes Volume: echte Ordner auf dem Wirt, per SFTP + # erreichbar. Nur lesend anfassen – zum Hineinlegen ist WebDAV der Weg, + # sonst kennt die Datenbank die Datei nicht. - ${DATA_PATH:-./data}:/data - networks: - - internal + networks: [internal] ports: - # Expose on loopback only — your external reverse proxy connects here. - # Host port via APP_PORT in .env (default 8080), container port stays 8080. + # Nur loopback. TLS und Domain gehören auf den Reverse Proxy des Wirts. - "127.0.0.1:${APP_PORT:-8080}:8080" postgres: @@ -66,71 +47,41 @@ services: POSTGRES_PASSWORD: ${DB_PASSWORD} volumes: - postgres_data:/var/lib/postgresql/data - networks: - - internal + networks: [internal] healthcheck: test: ["CMD-SHELL", "pg_isready -U ${DB_USER} -d ${DB_NAME}"] interval: 5s timeout: 5s retries: 10 - # ── Office (optional) ────────────────────────────────────────────────────── - # Laeuft nur mit `--profile office`. Ohne das Profil startet der Container - # nicht – wer kein Office will, soll nicht 1 GB Arbeitsspeicher dafuer - # abgeben, nur weil die Zeilen hier stehen. - # - # docker compose --profile office up -d - # - # Der Office-Server holt die Datei **selbst** beim Backend ab. Er muss Aether - # also unter PUBLIC_URL erreichen, nicht unter localhost. Im selben - # Compose-Netz erreicht er es unter `app:8080` – dafuer steht `extra_hosts` - # bzw. ein PUBLIC_URL, das auf den Wirt zeigt. + # Office – nur mit `--profile office`, kostet rund 1 GB Arbeitsspeicher. collabora: container_name: aether-collabora image: collabora/code:latest profiles: ["office"] restart: unless-stopped environment: - # Welche Hosts ihre Dateien hier oeffnen duerfen. Punkte maskiert, weil - # Collabora den Wert als regulaeren Ausdruck liest. - aliasgroup1: ${OFFICE_ALIASGROUP:-http://host.docker.internal:8090} - # Zwei getrennte Schalter, und beide zaehlen: - # - # ssl.enable – spricht Collabora selbst HTTPS? - # ssl.termination – endet HTTPS davor, an einem Reverse Proxy? - # - # Steht termination auf true, sagt Collabora dem Browser „verbinde dich - # per wss://" – auch dann, wenn davor gar kein HTTPS liegt. Im reinen - # HTTP-Testaufbau scheitert damit die WebSocket-Verbindung, und der - # Editor bleibt leer stehen, obwohl alles andere laedt (gemessen). - # - # Produktiv hinter deinem Reverse Proxy: OFFICE_SSL_TERMINATION=true. - # - # mount_jail_tree=false: Collabora sperrt jedes Dokument sonst in einen - # eigenen Einhaengepunkt und braucht dafuer CAP_SYS_ADMIN. Die Rechte - # dem Container zu geben waere der teurere Weg – ohne den Schalter - # scheitert der Start eines Dokuments mit „Failed to exec coolmount". + aliasgroup1: ${OFFICE_ALIASGROUP:-http://app:8080} + # ssl.termination=true nur, wenn davor wirklich HTTPS endet – sonst + # verlangt Collabora eine wss://-Verbindung, die es nicht gibt, und der + # Editor bleibt leer. mount_jail_tree=false spart CAP_SYS_ADMIN. extra_params: >- --o:ssl.enable=${OFFICE_SSL:-false} --o:ssl.termination=${OFFICE_SSL_TERMINATION:-false} --o:mount_jail_tree=false dictionaries: de_DE en_US - cap_add: - - MKNOD + cap_add: [MKNOD] extra_hosts: - # Damit der Container den Wirt erreicht – dort haengt der Reverse Proxy - # bzw. der veroeffentlichte Port von Aether. - "host.docker.internal:host-gateway" - networks: - - internal + networks: [internal] ports: - "127.0.0.1:${OFFICE_PORT:-9980}:9980" volumes: postgres_data: - # "data" ist bewusst weg – die Dateien liegen jetzt unter DATA_PATH auf dem - # Wirt (siehe oben). Die Datenbank bleibt ein benanntes Volume: An ihre - # Dateien geht man nicht von Hand, und Postgres mag fremde Besitzer nicht. + # "data" ist bewusst weg – die Dateien liegen unter DATA_PATH auf dem Wirt. + # Die Datenbank bleibt ein benanntes Volume: An ihre Dateien geht man nicht + # von Hand, und Postgres mag fremde Besitzer nicht. networks: internal: diff --git a/docker-compose.example.yml b/docker-compose.example.yml new file mode 100644 index 0000000..decb66c --- /dev/null +++ b/docker-compose.example.yml @@ -0,0 +1,87 @@ +# Aether – Betrieb aus dem fertigen Abbild. +# +# Diese Datei und .env.example genügen für eine neue Umgebung. Der Quelltext +# muss dort nicht liegen. +# +# cp docker-compose.example.yml docker-compose.yml +# cp .env.example .env # und ausfüllen +# docker login gitea.honeyy.duckdns.org +# docker compose up -d +# +# Hintergrund und Fallstricke: README.md +# Aus dem Quelltext bauen statt ziehen: deploy/docker-compose.yml + +services: + app: + container_name: aether-app + image: gitea.honeyy.duckdns.org/docker/aether:latest + restart: unless-stopped + env_file: .env + # Der Container läuft unter deiner Kennung, damit die Dateien unter + # DATA_PATH dir gehören und nicht einem Container-Nutzer. + user: "${APP_UID:-1000}:${APP_GID:-1000}" + environment: + # ocrmypdf legt Arbeitsdateien unter $HOME ab – das aus dem Image ist + # für einen abweichenden Nutzer nicht beschreibbar. + HOME: /tmp + depends_on: + postgres: + condition: service_healthy + volumes: + # Bind-Mount, kein benanntes Volume: Deine Dateien liegen als echte + # Ordner auf dem Wirt. Nur lesend anfassen – zum Hineinlegen ist WebDAV + # der Weg, sonst kennt die Datenbank die Datei nicht. + - ${DATA_PATH:-./data}:/data + networks: [internal] + ports: + # Nur loopback. TLS und Domain gehören auf deinen Reverse Proxy. + - "127.0.0.1:${APP_PORT:-8080}:8080" + + postgres: + container_name: aether-db + image: postgres:16-alpine + restart: unless-stopped + environment: + POSTGRES_DB: ${DB_NAME} + POSTGRES_USER: ${DB_USER} + POSTGRES_PASSWORD: ${DB_PASSWORD} + volumes: + - postgres_data:/var/lib/postgresql/data + networks: [internal] + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${DB_USER} -d ${DB_NAME}"] + interval: 5s + timeout: 5s + retries: 10 + + # Office – startet nur mit `docker compose --profile office up -d`. + # Kostet rund 1 GB Arbeitsspeicher; ohne Profil bleibt er aus. + # Die drei Adressen (OFFICE_URL / _PUBLIC_URL / _WOPI_URL) erklärt das README. + collabora: + container_name: aether-collabora + image: collabora/code:latest + profiles: ["office"] + restart: unless-stopped + environment: + aliasgroup1: ${OFFICE_ALIASGROUP:-http://app:8080} + # ssl.termination=true nur, wenn davor wirklich HTTPS endet – sonst + # verlangt Collabora eine wss://-Verbindung, die es nicht gibt, und der + # Editor bleibt leer. mount_jail_tree=false spart CAP_SYS_ADMIN. + extra_params: >- + --o:ssl.enable=${OFFICE_SSL:-false} + --o:ssl.termination=${OFFICE_SSL_TERMINATION:-false} + --o:mount_jail_tree=false + dictionaries: de_DE en_US + cap_add: [MKNOD] + extra_hosts: + - "host.docker.internal:host-gateway" + networks: [internal] + ports: + - "127.0.0.1:${OFFICE_PORT:-9980}:9980" + +volumes: + postgres_data: + +networks: + internal: + driver: bridge