SSD Nodes Learn Hosting plans →
Anleitungen Matt ConnorVon Matt Connor · Aktualisiert 2026-08-27

AFFiNE selbst hosten mit Docker Compose

AFFiNE auf einem VPS mit Docker Compose betreiben: vier Container, feste Image-Tags, Datenpfade, Backups und was 2 GB RAM in der Praxis leisten.

Was Sie beim Self-Hosting von AFFiNE erhalten

Mit dem Self-Hosting von AFFiNE erhalten Sie einen Workspace im Stil von Notion auf einem von Ihnen kontrollierten Server. Er läuft als vier Container: die Anwendung, ein einmaliger Migrationsjob, Postgres und Redis. Die Echtzeit-Zusammenarbeit ist enthalten. Ein selbst gehosteter Workspace unterstützt standardmäßig bis zu 10 Benutzer. Für die Installation benötigen Sie eine Compose-Datei und eine JSON-Konfigurationsdatei. Sie müssen die Image-Tags, das Festplattenlayout, die Speicherobergrenze und den vorgeschalteten Proxy festlegen.

AFFiNE kombiniert einen Dokumenteditor und eine unendliche Arbeitsfläche in einem Workspace. Eine Seite kann daher als Dokument gelesen oder als Whiteboard erweitert werden. Wenn Sie noch entscheiden, was Sie betreiben möchten, lesen Sie zuerst den Vergleich selbst gehosteter Notion-Alternativen. Dieser Leitfaden setzt voraus, dass die Entscheidung getroffen ist. Er behandelt den ordnungsgemäßen Betrieb von AFFiNE und vergleicht die Lösungen nicht erneut.

Alle Angaben wurden am 8 August 2026 anhand der Self-Hosting-Dokumentation von AFFiNE und der veröffentlichten Release-Dateien geprüft. Das neueste stabile Release an diesem Datum war 0.27.3. Es wurde am 23 July 2026 veröffentlicht.

Die tatsächlichen Aufgaben der vier Container

affine enthält den Server und den Webclient in einem Image. Er lauscht auf Port 3010.

affine_migration ist ein einmaliger Job. Er führt node ./scripts/self-host-predeploy.js aus, wendet die Datenbankmigrationen an und wird anschließend beendet. Die Anwendung definiert condition: service_completed_successfully für diesen Job. Wenn eine Migration mit einem Status ungleich 0 beendet wird, startet affine überhaupt nicht. Wenn die Weboberfläche nicht startet, sollten Sie zuerst das Log dieses Jobs prüfen.

postgres enthält Ihre Dokumente, Benutzer, Arbeitsbereiche und Berechtigungen. Das bereitgestellte Image ist pgvector/pgvector:pg16. Es handelt sich um gewöhnliches Postgres 16 mit einkompilierter pgvector-Erweiterung. pgvector fügt Postgres einen vector-Spaltentyp hinzu. Dieser numerische Datentyp speichert Embeddings, damit Text nach seiner Bedeutung durchsucht werden kann.

redis ist eine harte Abhängigkeit. Sowohl der Server als auch der Migrationsjob warten vor dem Start auf dessen Healthcheck. Beachten Sie, dass die bereitgestellte Compose-Datei Redis keinen Volume zuweist. Nichts darin bleibt nach einem docker compose down erhalten. Daraus ist eindeutig ersichtlich, dass Redis keine Ihrer Inhalte enthält und nicht gesichert werden muss.

Warum das Postgres-Image pgvector und nicht das Standard-Postgres-Image ist

Die Anforderung kommt aus dem Schema von AFFiNE und nicht aus einer Präferenz. In schema.prisma deklariert die Datenquelle extensions = [pgvector(map: "vector")], und vier Tabellen enthalten eine embedding-Spalte vom Typ vector(1024). Der Migrationsjob erstellt diese Tabellen unabhängig davon, ob Sie die AI-Funktionen jemals aktivieren. Die Erweiterung muss daher bereits in der Datenbank vorhanden sein, bevor die Migration abgeschlossen werden kann. Wenn Sie postgres:16 einsetzen, fehlt die Erweiterung. Die Migration kann diese Spalten dann nicht erstellen, und der Server wartet auf einen Job, der bereits fehlgeschlagen ist.

AFFiNE verwendet das pgvector-Image ab Version 0.21. Bei einer älteren Installation reicht es daher nicht aus, nur die Image-Zeile zu ändern. Lesen Sie vor dem Abruf eines Images die Upgrade-Seite in der AFFiNE-Dokumentation zum Self-Hosting.

Beachten Sie außerdem dieses Tag. pg16 steht für Postgres 16. Eine Major-Version von Postgres kann nicht einfach geändert werden. Wenn Sie ein vorhandenes Datenverzeichnis auf pg17 umstellen, verweigert Postgres den Start. In docker compose logs postgres erscheint dann eine Meldung wie The data directory was initialized by PostgreSQL version 16, which is not compatible with this version 17. Für den Wechsel der Major-Version benötigen Sie einen Dump und müssen ihn in ein neues Datenverzeichnis einspielen.

Wie viel CPU und RAM benötigt selbst gehostetes AFFiNE

Auf der Anforderungsseite von AFFiNE werden mindestens 4 CPU-Kerne und 2 GB RAM verlangt. Bei Dokumenten mit mehr als 10,000 Wörtern steigt der Speicherbedarf auf 4 GB. Auf derselben Seite wird erklärt, wofür der Speicher benötigt wird: das Synchronisierungssystem und das Zusammenführen von Dokumenten. Eine Zahl sollten Sie sich merken: Beim Zusammenführen eines Dokuments mit 10,000 Änderungen kann der Speicherbedarf auf 1 GB ansteigen.

Betrachten Sie das nun im Zusammenhang mit einem 2-GB-Tarif und zwei Personen, die gleichzeitig schreiben. Der Durchschnitt ist unproblematisch. Postgres und der Node-Prozess bleiben mit etwas Reserve unter dem Limit. Problematisch ist der Spitzenbedarf. Ein einzelner großer Merge kann zusätzlich zu allem bereits belegten Speicher 1 GB anfordern. Auf einem System mit 2 GB RAM und ohne Swap beantwortet der Out-of-Memory-Killer (OOM-Killer) diese Anforderung, indem er den größten Prozess beendet. Das ist der AFFiNE-Server.

Ihr Kollege sieht keine Fehlermeldung. Er sieht nur, dass die Seite neu geladen wird, weil restart: unless-stopped den Container innerhalb weniger Sekunden wieder startet. Raten Sie nicht, sondern prüfen Sie es:

docker inspect affine_server --format '{{.State.OOMKilled}} {{.RestartCount}}'
sudo dmesg -T | grep -i -E 'out of memory|killed process'

true aus dem ersten Befehl oder eine Killed process-Zeile mit node aus dem zweiten Befehl bedeutet, dass der Speicher ausgegangen ist und kein Fehler in der Anwendung vorliegt. Beheben Sie das Problem von beiden Seiten. Fügen Sie zuerst Swap hinzu. Dadurch wird ein Speicheranstieg langsam statt fatal:

sudo fallocate -l 2G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
free -h

free -h sollte nun eine Swap-Gesamtgröße von 2.0Gi melden. Swap macht AFFiNE nicht schneller und ist auch nicht dafür gedacht. Ein einsekündiger Spitzenbedarf wird dadurch zu einer langsamen Sekunde statt zu einem beendeten Container. Der andere Teil der Lösung besteht darin, zu verhindern, dass Postgres seinen Cache in den Speicherbereich ausdehnt, den die Anwendung beim Zusammenführen benötigt. Dafür sind Speicherlimits für einen Compose-Dienst vorgesehen.

Der Speicherbedarf auf dem Datenträger lässt sich deutlich leichter prognostizieren. Dies sind die Zahlen, die AFFiNE auf derselben Seite veröffentlicht:

ChartPublished AFFiNE storage figures, August 2026
The data behind this chart
[
  {
    "label": "Server install",
    "gb": 1.5
  },
  {
    "label": "Postgres per 1,000 docs",
    "gb": 0.1
  },
  {
    "label": "Blob store per 1,000 uploads",
    "gb": 10
  }
]

Die Serverinstallation benötigt 1.5 GB. Tausend Dokumente mit jeweils ungefähr tausend Wörtern fügen 0.1 GB an Postgres-Daten hinzu, also nahezu nichts. Tausend hochgeladene Dateien fügen 10 GB hinzu. Das ist der entscheidende Faktor. Dies sind veröffentlichte Planungswerte und keine Messungen einer laufenden Instanz. Betrachten Sie sie daher als Größenordnung und nicht als Zusage. Entscheidend ist das Verhältnis: Ihre Datenbank bleibt klein, und Ihre Uploads bestimmen den Speicherbedarf auf dem Datenträger.

Schreiben Sie die Compose-Datei selbst und pinnen Sie die Tags

Die dokumentierte Installation lädt mit curl -L -o docker-compose.yml https://github.com/toeverything/AFFiNE/releases/latest/download/docker-compose.yml eine fertige Datei herunter. Das funktioniert. Ein Detail sollten Sie kennen, bevor Sie sich darauf verlassen: Am 8. August 2026 liest die Datei aus Release 0.27.3 ihre Pfade noch aus einer .env-Datei, und zwar über ${UPLOAD_LOCATION}, ${CONFIG_LOCATION} und ${DB_DATA_LOCATION}. Die Referenzseite der Dokumentation zeigt dagegen bereits ein neueres Layout, das alles unter ./data hält und überhaupt keine .env benötigt. Beide Varianten sind korrekt. Wenn Sie die Datei selbst schreiben, ist diese Frage geklärt. Außerdem müssen Sie die Datei ohnehin bearbeiten, um die Images zu pinnen und ein Datenbankpasswort festzulegen.

mkdir -p ~/affine/config ~/affine/data
cd ~/affine
printf 'DB_PASSWORD=%s\n' "$(openssl rand -hex 24)" > .env
chmod 600 .env

Compose liest .env im Projektverzeichnis selbstständig ein und ersetzt ${DB_PASSWORD} für Sie. Das Passwort erscheint dadurch nie in der Datei, die Sie in einen Support-Thread kopieren würden. Diese Vorgehensweise sollten Sie für jeden Stack beibehalten. Die Begründung finden Sie unter Secrets aus der Compose-Datei heraushalten.

Schreiben Sie jetzt ~/affine/docker-compose.yml:

name: affine
services:
  affine:
    image: ghcr.io/toeverything/affine:stable
    container_name: affine_server
    ports:
      - '127.0.0.1:3010:3010'
    depends_on:
      redis:
        condition: service_healthy
      postgres:
        condition: service_healthy
      affine_migration:
        condition: service_completed_successfully
    volumes:
      - ./data/storage:/root/.affine/storage
      - ./config:/root/.affine/config
    environment:
      - REDIS_SERVER_HOST=redis
      - DATABASE_URL=postgresql://affine:${DB_PASSWORD}@postgres:5432/affine
      - AFFINE_INDEXER_ENABLED=false
    restart: unless-stopped

  affine_migration:
    image: ghcr.io/toeverything/affine:stable
    container_name: affine_migration_job
    command: ['sh', '-c', 'node ./scripts/self-host-predeploy.js']
    volumes:
      - ./data/storage:/root/.affine/storage
      - ./config:/root/.affine/config
    environment:
      - REDIS_SERVER_HOST=redis
      - DATABASE_URL=postgresql://affine:${DB_PASSWORD}@postgres:5432/affine
      - AFFINE_INDEXER_ENABLED=false
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy

  redis:
    image: redis:8-alpine
    container_name: affine_redis
    healthcheck:
      test: ['CMD', 'redis-cli', '--raw', 'incr', 'ping']
      interval: 10s
      timeout: 5s
      retries: 5
    restart: unless-stopped

  postgres:
    image: pgvector/pgvector:pg16
    container_name: affine_postgres
    volumes:
      - ./data/postgres:/var/lib/postgresql/data
    environment:
      POSTGRES_USER: affine
      POSTGRES_PASSWORD: ${DB_PASSWORD}
      POSTGRES_DB: affine
      POSTGRES_INITDB_ARGS: '--data-checksums'
    healthcheck:
      test: ['CMD', 'pg_isready', '-U', 'affine', '-d', 'affine']
      interval: 10s
      timeout: 5s
      retries: 5
    restart: unless-stopped

Die Datei unterscheidet sich in vier Punkten von der Datei, die das Upstream-Projekt bereitstellt. Jeder Punkt hat einen Grund.

  • 127.0.0.1:3010:3010 veröffentlicht den Port nur auf der Loopback-Adresse. Dadurch kann außerhalb des Servers niemand auf AFFiNE zugreifen, bis Sie den Zugriff festlegen. '3010:3010' aus der Upstream-Datei bindet dagegen an jedes Interface. Bei den meisten VPS-Images gehört dazu auch das öffentliche Interface.
  • POSTGRES_HOST_AUTH_METHOD: trust wurde entfernt und stattdessen ein Passwort festgelegt. Die Trust-Authentifizierung akzeptiert jede Verbindung zu dieser Datenbank als Benutzer affine ohne Passwort. Das ist auf das private Compose-Netzwerk beschränkt. Das bleibt unproblematisch, bis Sie eines Tages einen weiteren Container mit diesem Netzwerk verbinden oder beim Debugging Port 5432 veröffentlichen.
  • redis:8-alpine ersetzt ein einfaches redis, das zu latest aufgelöst wird. Im August 2026 ist das Redis 8. Durch das Pinning bleibt die getestete Hauptversion erhalten. Außerdem verhindert es, dass bei einem unabhängigen docker compose pull-Vorgang unerwartet Redis 9 installiert wird.
  • pgvector/pgvector:pg16 bleibt genau so gesetzt wie im Upstream-Projekt, aus dem oben genannten Grund.

POSTGRES_PASSWORD wird nur gelesen, wenn Postgres sein Datenverzeichnis zum ersten Mal erstellt. Bei einer bereits vorhandenen Instanz setzen Sie das Passwort mit docker compose exec postgres psql -U affine -c "ALTER USER affine WITH PASSWORD 'yourpassword'" und passen anschließend DATABASE_URL entsprechend an.

Die Konfiguration befindet sich in config/config.json

AFFiNE liest seine Einstellungen aus config/config.json. Das ist das Verzeichnis, das Sie unter /root/.affine/config eingebunden haben. Diese Datei wird nicht automatisch erstellt. Legen Sie sie daher vor dem ersten Start an. Öffnen Sie ~/affine/config/config.json in einem Editor und tragen Sie diesen Inhalt ein. Ersetzen Sie die Beispieldomain durch Ihre eigene Domain:

{
  "$schema": "https://github.com/toeverything/affine/releases/latest/download/config.schema.json",
  "server": {
    "name": "Team workspace",
    "externalUrl": "https://affine.example.com"
  },
  "copilot": {
    "enabled": false,
    "byok": {
      "enabled": false
    }
  }
}

server.externalUrl muss die Adresse sein, die Ihre Benutzer tatsächlich im Browser öffnen. AFFiNE erstellt daraus Freigabelinks und Einladungen zu Workspaces. Wenn dort weiterhin http://localhost:3010 steht, verweist eine von Ihnen gesendete Einladung auf den Computer des Empfängers und funktioniert dort nicht. Setzen Sie den Wert vor dem ersten Start auf die öffentliche HTTPS-Adresse. Dadurch stimmen die Datei und das Administrationspanel immer überein.

copilot steuert die KI-Funktionen. copilot.byok.enabled aktiviert die Verwendung eines eigenen Schlüssels. Damit kann der Besitzer eines Workspaces in den Workspace-Einstellungen den Schlüssel seines eigenen Modellanbieters einfügen. Beim Self-Hosting von AFFiNE ist kein KI-Abonnement enthalten. Lassen Sie beide Optionen auf false, wenn Sie die Funktion nicht verwenden möchten.

Starten Sie den Stack:

docker compose up -d
docker compose ps

docker compose ps sollte affine_postgres und affine_redis als gesund, affine_server als laufend und affine_migration_job mit dem Status exited (0) anzeigen. Bei jedem anderen Exit-Code des Migrationsjobs sollten Sie die Ursache untersuchen. Das Log nennt den Schritt, bei dem der Vorgang angehalten wurde:

docker compose logs affine_migration

Fixieren Sie das Image, bevor Sie es vergessen

stable ist ein veränderlicher Tag. Der Release-Workflow von AFFiNE weist bei jedem stabilen Build mehrere Tags zu. Zwei davon sind hier relevant: stable, das bei jedem Release neu gesetzt wird, und stable- gefolgt vom kurzen Git-Hash, das unverändert bleibt. Wenn stable verwendet wird, lädt ein docker compose pull sechs Monate später ein anderes Image und führt dessen Migrationen zu einem Zeitpunkt gegen Ihre Datenbank aus, den Sie nicht festgelegt haben. Fixieren Sie das exakt getestete Image:

docker compose pull
docker image inspect ghcr.io/toeverything/affine:stable --format '{{index .RepoDigests 0}}'

Der Befehl gibt eine Zeile wie ghcr.io/toeverything/affine@sha256: gefolgt von einem langen Hash aus. Fügen Sie die gesamte Zeichenfolge in die image:-Zeile von beiden affine und affine_migration ein. Diese beiden Werte müssen immer übereinstimmen, weil dasselbe Image zwei Rollen übernimmt. Eine Abweichung bedeutet, dass die Datenbank auf ein Schema migriert wird, während sie mit einem anderen Schema bedient wird. Ein Upgrade ist dann eine bewusste Änderung statt einer Überraschung: Ändern Sie den Digest, erstellen Sie ein Backup, docker compose pull, docker compose up -d.

Administratorkonto anlegen, bevor es jemand anderes tut

Öffnen Sie /admin auf einer frischen Instanz. AFFiNE leitet Sie zu einer Seite für die Kontoerstellung weiter, weil auf dem Server noch kein Administrator vorhanden ist. In diesem Ablauf gibt es weder einen Einladungscode noch ein Setup-Token. Die erste Person, die diese Seite lädt, wird Administrator Ihres Servers. Deshalb muss der Port geschlossen bleiben, bis Sie sich registriert haben.

Aus diesem Grund bindet die Compose-Datei oben an 127.0.0.1. Greifen Sie über einen SSH-Tunnel von Ihrem eigenen Rechner darauf zu:

ssh -L 3010:127.0.0.1:3010 you@your-server-ip

Lassen Sie den Tunnel geöffnet und rufen Sie http://127.0.0.1:3010/admin im lokalen Browser auf. Registrieren Sie sich und melden Sie sich an. Schließen Sie anschließend den Tunnel. Erst jetzt können Sie die Instanz sicher unter einem öffentlichen Namen erreichbar machen. Dasselbe Rennen tritt auch bei anderen selbst gehosteten Anwendungen auf. Es ist problematischer, wenn beim ersten Login ein an den Hostnamen gebundener Passkey erstellt wird. Deshalb müssen TLS und die endgültige Domain feststehen, bevor das erste Konto angelegt wird, wenn Sie openGym selbst hosten.

Wo AFFiNE Ihre Daten speichert

Drei Pfade enthalten alle Daten. Sie liegen innerhalb des Verzeichnisses, das Sie erstellt haben.

  • ./data/postgres ist das Datenverzeichnis von Postgres. Es enthält Dokumente, Benutzer, Workspaces und Berechtigungen.
  • ./data/storage wird im Container unter /root/.affine/storage eingebunden und enthält alle hochgeladenen Dateien.
  • ./config wird unter /root/.affine/config eingebunden und enthält config.json.

Das Upstream-Projekt verwendet hier Bind-Mounts statt benannter Volumes. Diese Entscheidung ist beabsichtigt: Sie können diese Pfade mit gewöhnlichen Befehlen tar-en und kopieren, ohne Docker nach deren Speicherort fragen zu müssen. Der Nachteil ist, dass Sie nun für die Dateibesitzrechte auf dem Host verantwortlich sind. Dieser Kompromiss wird unter Bind-Mounts und benannte Volumes erläutert.

So erstellen Sie ein Backup von AFFiNE

Es müssen zwei Dinge gesichert werden, und zwar auf unterschiedliche Weise. Die Datenbank ist ein laufender Server. Wenn Sie ihre Dateien im laufenden Betrieb kopieren, ist die Kopie beschädigt. Erstellen Sie stattdessen einen Dump:

mkdir -p ~/affine/backup
cd ~/affine
docker compose exec -T postgres pg_dump --format c --username affine affine \
  > backup/affine-$(date +%F).dump
ls -lh backup/

Der Dump wird im Container über seinen lokalen Socket ausgeführt. Deshalb werden Sie nicht zur Eingabe des Passworts aufgefordert. Prüfen Sie die Größe in der Ausgabe von ls. Eine Datei mit einigen hundert Bytes bedeutet, dass der Dump fehlgeschlagen ist, während die Shell die Datei trotzdem angelegt hat. Diesen Fehler bemerkt man oft erst sechs Monate später. Auch -T ist wichtig: Ohne diese Option kann Compose ein Terminal zuweisen und den Binärdatenstrom beschädigen.

Hochgeladene Dateien sind normale Dateien. Packen Sie sie daher mit tar:

tar czf backup/storage-$(date +%F).tgz -C data storage
cp config/config.json backup/config-$(date +%F).json

Bewahren Sie config.json manuell in Ihrem Backup auf. In der AFFiNE-Dokumentation wird der Konfigurationsexport aus dem Admin-Panel weiterhin als nicht implementiert aufgeführt; dies wurde im August 2026 geprüft. Daher ist die Datei auf dem Datenträger die einzige Kopie Ihrer Einstellungen. Kopieren Sie alle drei Dateien vom Server herunter. Ein Backup auf derselben Festplatte wie die zu schützenden Daten ist kein Backup. Diese Aufteilung aus einem Datenbank-Dump und einem tar-Archiv des Uploads-Verzeichnisses ist das Muster, das Sie für jeden anderen zustandsbehafteten Container wiederholen sollten. Dasselbe Verfahren schützt auch den Gesprächsverlauf und die Anhänge, wenn Sie Chatwoot als Support-Desk selbst hosten.

Wiederherstellung und eine Falle in den veröffentlichten Schritten

Lesen Sie die offiziellen Schritte zur Wiederherstellung, bevor Sie sie benötigen, und prüfen Sie sie genau. In der im August 2026 veröffentlichten Fassung wird eine Datei namens affine.backup in den Container kopiert und anschließend aus ./pg.backup wiederhergestellt. Das sind zwei unterschiedliche Namen. Außerdem wird ein Verzeichnis ./postgres entfernt, während die aktuelle Compose-Datei ihre Daten in ./data/postgres speichert. Verwenden Sie die Pfade, die Sie tatsächlich eingerichtet haben, und nicht die Pfade aus dem Beispiel. Für das Layout in dieser Anleitung lautet die Abfolge:

cd ~/affine
docker compose down
sudo mv data/postgres data/postgres.old
docker compose up -d postgres
docker compose cp backup/affine-2026-08-08.dump postgres:/tmp/affine.dump
docker compose exec postgres pg_restore --format c --username affine \
  --dbname affine --verbose /tmp/affine.dump
docker compose up -d

Beachten Sie mv statt rm. Wenn Sie keine Kopie der Datenbank aufbewahrt haben, kann ein einziger falscher Befehl zu einem vollständigen Datenverlust führen. Das Verschieben des alten Verzeichnisses an einen anderen Ort kostet dagegen nichts. Stellen Sie auch die Uploads mit tar xzf backup/storage-2026-08-08.tgz -C data wieder her. Andernfalls werden alle Dokumente mit fehlerhaften Anhängen angezeigt. Melden Sie sich anschließend an und öffnen Sie ein Dokument, das ein Bild enthält. Das ist der Test. Eine Wiederherstellung, die Sie nicht im Browser geöffnet haben, ist eine Datei, aber keine Sicherung.

AFFiNE hinter einem bereits betriebenen Proxy

AFFiNE verwendet WebSocket, und das ist nicht optional. Die Dokumentation formuliert es eindeutig: WebSocket ist die Grundlage des Synchronisierungs- und Kollaborationssystems von AFFiNE. Ein Proxy, der diese Verbindungen nicht hochstuft, führt zu einem Workspace, in dem Bearbeitungen unbemerkt nicht mehr synchronisiert werden. Die Seite wird geladen, die Anmeldung funktioniert, und eine Bearbeitung in einem Browser erreicht den anderen nicht. Öffnen Sie in den Entwicklertools Ihres Browsers den Tab Network und filtern Sie nach WS. Eine Verbindung, die wiederholt geöffnet und geschlossen wird, weist darauf hin, dass der Proxy das Upgrade nicht weiterleitet.

Wenn Sie Traefik bereits für andere Container betreiben, wird AFFiNE als normaler Dienst hinzugefügt. Löschen Sie den ports:-Block aus dem Dienst affine und fügen Sie anschließend Folgendes hinzu:

    networks:
      - default
      - proxy
    labels:
      - 'traefik.enable=true'
      - 'traefik.docker.network=proxy'
      - 'traefik.http.routers.affine.rule=Host(`affine.example.com`)'
      - 'traefik.http.routers.affine.entrypoints=websecure'
      - 'traefik.http.routers.affine.tls.certresolver=letsencrypt'
      - 'traefik.http.services.affine.loadbalancer.server.port=3010'

und am Ende der Datei, neben services::

networks:
  proxy:
    external: true

Der Name des Certificate Resolvers muss mit dem in Ihrer Traefik-Konfiguration definierten Namen übereinstimmen. loadbalancer.server.port ist der Container-Port 3010, niemals ein Host-Port. Traefik proxyt WebSocket-Verbindungen ohne zusätzliche Konfiguration. Sie müssen daher nichts weiter hinzufügen. Wenn der Rest Ihres Stacks bereits hinter Authentik für Single Sign-on betrieben wird, können Sie den Browserzugriff auf AFFiNE mit einer Forward-Auth-Middleware an diesem Router absichern. Lassen Sie sie jedoch deaktiviert, bis Sie die Desktop-App getestet haben. Diese verfügt über keine Browser-Session und schlägt bei der Synchronisierung andernfalls einfach fehl. Der Betrieb mehrerer Apps hinter einer Instanz wird unter einem einzelnen Traefik vor mehreren Apps beschrieben.

Bei nginx müssen Sie das Upgrade explizit anfordern:

location / {
    proxy_pass http://127.0.0.1:3010;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    client_max_body_size 100m;
}

client_max_body_size ist in nginx standardmäßig auf 1 MB gesetzt. Ohne diese Zeile schlägt daher jeder Upload fehl, der größer als ein kleines Foto ist. Der Server antwortet mit dem Status 413, und in den AFFiNE-Logs erscheint nichts, weil die Anfrage den Dienst nie erreicht hat. Caddy benötigt eine Zeile, reverse_proxy http://127.0.0.1:3010, und übernimmt Zertifikate sowie WebSocket-Upgrades selbst.

Was der Self-Hosted-Betrieb nicht bietet

Seien Sie sich dessen bewusst, bevor Sie ein Team umstellen.

Echtzeit-Zusammenarbeit ist vorhanden. Auf diese Funktion beziehen sich auch die Größenempfehlungen, da die eigene Dokumentation von AFFiNE den Speicherbedarf dem Synchronisierungssystem und dem Zusammenführen von Dokumenten zuschreibt. Offline-Bearbeitung ist der Grund, warum viele ein Local-First-Tool einsetzen möchten. Die Desktop-Anwendung kann Ihren Self-Hosted-Server zur Workspace-Liste hinzufügen und sich dort anmelden. Testen Sie das genaue Offline-Verhalten, auf das Ihr Team angewiesen ist, bevor Sie sich festlegen: Bearbeiten Sie ein Dokument in der Desktop-Anwendung bei deaktivierter Netzwerkverbindung, stellen Sie die Verbindung wieder her und prüfen Sie anschließend das Ergebnis auf einem zweiten Gerät. Feature-Listen sind kein Beleg. Das gilt auch für diese Liste.

Die serverseitige Volltextsuche ist in der mitgelieferten Compose-Datei deaktiviert. Dort ist AFFINE_INDEXER_ENABLED=false auf dem Server und im Migrationsjob gesetzt. Wenn Sie die Suche aktivieren, müssen Sie einen Manticore Search-Container hinzufügen. Damit kommt ein fünfter Dienst und zusätzlicher Speicherbedarf hinzu. Auf einem System mit 2 GB RAM ist das die Änderung, durch die die verfügbaren Ressourcen nicht mehr ausreichen. Die Suche innerhalb des Clients funktioniert weiterhin für den geöffneten Workspace.

Vor dem Einladen weiterer Benutzer sollten Sie zwei Einschränkungen kennen. Ein Self-Hosted-Workspace kann höchstens 10 Sitze haben. Für mehr Sitze benötigen Sie eine Team-Lizenz von AFFiNE. Unbegrenzter Blob-Speicher und eine unbegrenzte Blob-Größe für Self-Hosted-Instanzen werden in der Dokumentation als vorgesehen, aber noch nicht vollständig implementiert beschrieben. Dies wurde im August 2026 geprüft. Für einen Haushalt oder ein kleines Team sind diese Einschränkungen nicht relevant. Beide sind relevant, wenn Sie vierzig Personen umstellen möchten.

Upgrades

Lesen Sie zuerst die Release Notes, insbesondere vor einem Minor-Versionssprung wie von 0.26 auf 0.27, bei dem inkompatible Änderungen eingeführt werden können. Sichern Sie die Datenbank und das Speicherverzeichnis, bevor Sie Änderungen vornehmen, da der Migrationsjob beim nächsten Start Ihr Schema ändert und sich dies nicht rückgängig machen lässt. Ändern Sie anschließend den festgelegten Digest, führen Sie docker compose pull und danach docker compose up -d aus und überwachen Sie docker compose logs -f affine_migration, bis der Befehl ordnungsgemäß beendet wird. docker image prune entfernt anschließend die alten Layer. Ein historischer Hinweis für Installationen mit sehr altem Stand: Ab Version 0.23.0 wurde der Imagename von affine-graphql in affine geändert. Eine ältere Compose-Datei muss daher vor dem Pull in ihren Image-Zeilen angepasst werden, damit das Image gefunden wird.

FAQ

Warum startet der AFFiNE-Container nie?

Der Dienst affine definiert condition: service_completed_successfully für den Job affine_migration. Wenn die Migration mit einem anderen Status als 0 beendet wird, startet der Server nie und es erscheint überhaupt keine Weboberfläche. Führen Sie docker compose logs affine_migration aus, um zu sehen, bei welchem Schritt der Vorgang angehalten wurde. Die häufigste Ursache in einer manuell bearbeiteten Compose-Datei ist ein unverändertes postgres-Image anstelle von pgvector/pgvector:pg16. Das AFFiNE-Schema definiert die pgvector-Erweiterung und erstellt Tabellen mit vector(1024)-Spalten, die reines Postgres nicht anlegen kann.

Wie viel RAM benötigt selbst gehostetes AFFiNE?

Auf der Anforderungsseite von AFFiNE werden mindestens 4 CPU-Kerne und 2 GB RAM genannt. Bei Dokumenten mit mehr als 10,000 Wörtern steigt der Bedarf auf 4 GB. Außerdem wird darauf hingewiesen, dass das Zusammenführen eines Dokuments mit 10,000 Änderungen kurzzeitig 1 GB belegen kann. Auf einem Server mit 2 GB ist diese Lastspitze das Problem, nicht die Leerlauflast. Der Out-of-Memory-Killer des Kernels beendet den AFFiNE-Prozess, und restart: unless-stopped startet ihn erneut. Benutzer sehen deshalb nur, dass die Seite neu geladen wird, statt eine Fehlermeldung zu erhalten. Prüfen Sie dies mit docker inspect affine_server --format '{{.State.OOMKilled}}' und sudo dmesg -T | grep -i 'out of memory'. Legen Sie anschließend eine 2-GB-Swap-Datei an, damit eine Lastspitze den Server verlangsamt und nicht zum Prozessabbruch führt.

Wo speichert AFFiNE meine Daten, und was muss ich sichern?

Drei Pfade unterhalb Ihres Compose-Verzeichnisses enthalten alle Daten: ./data/postgres für die Datenbank, ./data/storage für hochgeladene Dateien und ./config für config.json. Sichern Sie die Datenbank mit docker compose exec -T postgres pg_dump --format c --username affine affine > affine.dump, statt die Dateien zu kopieren. Ein laufendes Postgres lässt sich nicht sicher durch Kopieren sichern. Packen Sie ./data/storage für die Uploads mit tar und bewahren Sie eine manuelle Kopie von config.json auf. Der Export der Konfiguration aus dem Administrationsbereich war im August 2026 laut Dokumentation noch nicht implementiert.

Funktioniert die Echtzeitzusammenarbeit mit einem selbst gehosteten AFFiNE?

Ja. Dafür muss nichts aktiviert werden. Die einzige Voraussetzung betrifft Ihren Reverse Proxy, weil die Synchronisierung über WebSocket-Verbindungen läuft. Bei nginx bedeutet das proxy_http_version 1.1 sowie die Header Upgrade und Connection: upgrade. Traefik und Caddy leiten diese Verbindungen ohne zusätzliche Konfiguration weiter. Wenn der Proxy kein Upgrade dieser Verbindungen durchführt, lädt der Workspace normal und die Anmeldung funktioniert. Änderungen, die in einem Browser vorgenommen werden, erscheinen jedoch nie in einem anderen Browser.

Kann ich AFFiNE mit einem unveränderten Postgres-Image betreiben?

Nein. schema.prisma von AFFiNE definiert extensions = [pgvector(map: "vector")] und vier Tabellen mit einer embedding-Spalte vom Typ vector(1024). Der Migrationsjob erstellt diese Tabellen auch dann, wenn die KI-Funktionen deaktiviert sind. Verwenden Sie pgvector/pgvector:pg16. Dabei handelt es sich um Postgres 16 mit einkompilierter Erweiterung. Wenn Sie AFFiNE stattdessen auf einen externen Postgres-Server verweisen, installieren Sie pgvector auf diesem Server und erstellen Sie die Erweiterung in der Zieldatenbank, bevor Sie die Migration ausführen.