SSD Nodes Learn Hosting plans →
Anleitungen Matt ConnorVon Matt Connor

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 verwalteten Server. AFFiNE läuft dabei in vier Containern: der Anwendung, einem einmalig ausgeführten Migrationsjob, Postgres und Redis. Die Echtzeit-Zusammenarbeit ist enthalten, und zwar für bis zu 10 Benutzer, die ein selbst gehosteter Workspace standardmäßig unterstützt. Die Installation besteht aus einer Compose-Datei und einer JSON-Konfigurationsdatei. Zu planen sind vor allem die Image-Tags, das Festplattenlayout, die Speicherobergrenze und der Proxy, den Sie davor schalten.

AFFiNE vereint einen Dokumenteditor und eine unendliche Arbeitsfläche in einem Workspace. Eine Seite kann dadurch als Dokument gelesen oder als Whiteboard erweitert werden. Wenn Sie noch entscheiden, welche Lösung Sie einsetzen möchten, lesen Sie zuerst den Vergleich selbst gehosteter Notion-Alternativen. In diesem Leitfaden ist die Entscheidung bereits getroffen. Er beschreibt den ordnungsgemäßen Betrieb von AFFiNE und vergleicht die Lösungen nicht erneut.

Alle Angaben wurden am 8. August 2026 anhand der AFFiNE-Dokumentation zum Self-Hosting und der veröffentlichten Release-Dateien geprüft. Die neueste stabile Release-Version an diesem Datum war 0.27.3, veröffentlicht am 23. Juli 2026. Aussagen mit einem späteren Datum wurden am 12. Oktober 2026 erneut anhand ihrer Primärquellen geprüft. Zu diesem Zeitpunkt war 0.27.4 die neueste stabile Release-Version, veröffentlicht am 18. August 2026.

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 v0.27.4 deklariert schema.prisma weiterhin extensions = [pgvector(map: "vector")], enthält aber keine Embedding-Spalten. Die vier Modelle mit embedding-Spalten vom Typ vector(1024) aus v0.27.3 sind in v0.27.4 nicht mehr vorhanden. Verwenden Sie das mitgelieferte pgvector/pgvector:pg16-Image: Die Erweiterung ist weiterhin deklariert, und das Standard-postgres:16 enthält sie nicht.

Das offizielle Template für v0.27.4 verwendet das pgvector-Image. Das Template für v0.21.0 verwendete noch postgres:16. Bei einer älteren Installation ist das Ändern der Image-Zeile nicht das vollständige Upgrade. Lesen Sie daher die Upgrade-Seite in der AFFiNE-Dokumentation zum Self-Hosting, bevor Sie etwas pullen.

Noch ein Hinweis zu diesem Tag. pg16 bedeutet Postgres 16. Eine Postgres-Hauptversion ist keine Versionsnummer, die Sie einfach erhöhen können. Wenn Sie sie bei einem vorhandenen Datenverzeichnis in pg17 ändern, verweigert Postgres den Start. In docker compose logs postgres erscheint dann eine Zeile wie The data directory was initialized by PostgreSQL version 16, which is not compatible with this version 17. Für den Wechsel der Hauptversion ist eine der drei Methoden erforderlich, die in den Release Notes zu PostgreSQL 17 genannt werden: ein Dump und die Wiederherstellung in ein neues Datenverzeichnis, pg_upgrade oder logische Replikation. Bei einer einzelnen Compose-Installation sind Dump und Wiederherstellung meist am einfachsten, weil pg_upgrade die Binärdateien des alten und des neuen Servers parallel benötigt.

Wie viel CPU und RAM benötigt selbst gehostetes AFFiNE?

Die Anforderungsseite von AFFiNE nennt mindestens 4 CPU-Kerne und 2 GB RAM. Sobald Ihre Dokumente mehr als 10,000 Wörter enthalten, steigt der Speicherbedarf auf 4 GB. Auf derselben Seite wird erklärt, wofür der Speicher verwendet wird: für 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 kurzzeitig auf 1 GB steigen.

Betrachten Sie das nun im Zusammenhang mit einem 2-GB-Tarif, auf dem zwei Personen schreiben. Der Durchschnitt ist unproblematisch. Postgres und der Node-Prozess bleiben mit Reserve unter dem Limit. Problematisch ist der Spitzenwert. Ein einzelner großer Zusammenführungsvorgang kann den Speicherbedarf des Servers auf etwa 1 GB erhöhen. Auf einem System mit 2 GB RAM und ohne Swap kann dieser Spitzenwert zusammen mit Postgres und allen anderen residenten Prozessen den Speicher erschöpfen. Der Out-of-Memory-Killer (OOM-Killer) des Kernels beendet dann einen Prozess, um Speicher freizugeben. Der AFFiNE-Server ist als größter Prozess das wahrscheinlichste Ziel. In der Docker-Dokumentation wird jedoch ausdrücklich darauf hingewiesen, dass jeder Prozess ausgewählt werden kann, einschließlich Docker selbst.

Wenn es den AFFiNE-Server trifft, sieht Ihre Kollegin oder Ihr Kollege keine Fehlermeldung. Die Seite wird neu geladen, weil restart: unless-stopped den Container innerhalb weniger Sekunden wieder startet. Raten Sie nicht, sondern prüfen Sie die Ursache:

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 dem Namen node aus dem zweiten Befehl bedeutet, dass der Speicher erschöpft war und kein Fehler im Programm vorlag. Beheben Sie das Problem an beiden Stellen. Fügen Sie zuerst Swap hinzu. Dadurch steht bei einem Speicheranstieg ein Puffer zur Verfügung, bevor der OOM-Killer eingreift:

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. Er bietet einem kurzen Speicheranstieg eine Ausweichmöglichkeit. Dadurch verursacht der Anstieg mit deutlich höherer Wahrscheinlichkeit eine kurze Verzögerung als einen beendeten Container. Swap ist ein Puffer, keine Garantie. Der zweite Teil der Lösung besteht darin, das Wachstum des Postgres-Caches in dem Speicherbereich zu begrenzen, den die Anwendung beim Zusammenführen benötigt. Dafür gibt es Speicherlimits für einen Compose-Dienst. Wenn die Prozesse danach weiterhin beendet werden, benötigt der Server bei größeren Dokumenten die 4 GB, die auf der AFFiNE-Anforderungsseite genannt werden.

Der Speicherplatzbedarf lässt sich wesentlich einfacher abschätzen. Dies sind die Zahlen, die AFFiNE auf derselben Seite veröffentlicht:

DiagrammPublished AFFiNE storage figures, August 2026
Die Daten hinter diesem Diagramm
[
  {
    "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 den Postgres-Daten 0.1 GB hinzu, also nahezu nichts. Tausend hochgeladene Dateien fügen 10 GB hinzu. Damit ist der wesentliche Punkt erklärt. Bei diesen Angaben handelt es sich um veröffentlichte Planungswerte und nicht um Messungen einer laufenden Instanz. Betrachten Sie sie daher als Größenordnung und nicht als Zusage. Entscheidend ist das Verhältnis: Ihre Datenbank bleibt klein, während Ihre Uploads den benötigten Speicherplatz bestimmen.

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. Bevor Sie sich darauf verlassen, sollten Sie ein Detail kennen: Am 8. August 2026 las die Datei aus dem Release 0.27.3 ihre Pfade noch aus einer .env-Datei. Dafür verwendete sie ${UPLOAD_LOCATION}, ${CONFIG_LOCATION} und ${DB_DATA_LOCATION}. Die Referenzseite der Dokumentation zeigte dagegen bereits ein neueres Layout. Darin liegen der Speicher und die PostgreSQL-Daten unter ./data. Die Konfiguration bleibt unter ./config. Eine .env-Datei ist dafür nicht erforderlich. Release 0.27.4 stellte die angehängte Datei auf dieses Layout um. Seit dem 12. Oktober 2026 stimmen beide überein. Wenn Sie aus der älteren Datei installiert haben, kommen Ihre Pfade weiterhin aus .env. Die Upgrade-Seite von AFFiNE warnt, dass ein Ersetzen der Compose-Datei ohne Übernahme dieser Einstellungen Postgres mit einem leeren Datenverzeichnis starten kann. Wenn Sie die Datei selbst schreiben, stellt sich diese Frage nicht. 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 selbstständig aus dem Projektverzeichnis und ersetzt ${DB_PASSWORD} für Sie. Das Passwort erscheint dadurch niemals in der Datei, die Sie in einen Support-Thread kopieren würden. Diese Vorgehensweise sollten Sie für jeden von Ihnen betriebenen Stack beibehalten. Die Begründung finden Sie unter Secrets aus der Compose-Datei heraushalten.

Schreiben Sie nun ~/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 der Upstream-Anbieter ausliefert. Für jeden Punkt gibt es einen Grund.

  • 127.0.0.1:3010:3010 veröffentlicht den Port nur auf der Loopback-Adresse. Docker-Versionen vor 28.0.0 erlauben Hosts im selben L2-Segment, auf lokal veröffentlichte Ports zuzugreifen. Bei konfiguriertem Direct Routing kann der Container-Port trotz Veröffentlichung auf Loopback ebenfalls erreichbar sein. Verwenden Sie Docker 28.0.0 oder neuer und lassen Sie Direct Routing während der Einrichtung deaktiviert. Das Upstream-'3010:3010' bindet an jede Schnittstelle. Bei den meisten VPS-Images gehört dazu auch die öffentliche Schnittstelle.
  • POSTGRES_HOST_AUTH_METHOD: trust wurde entfernt und stattdessen ein Passwort gesetzt. Die Trust-Authentifizierung akzeptiert jede Verbindung zu dieser Datenbank als Benutzer affine, ohne ein Passwort zu verlangen. Sie ist auf das private Compose-Netzwerk beschränkt. Das ist unproblematisch, bis Sie eines Tages einen weiteren Container mit diesem Netzwerk verbinden oder während der Fehlersuche 5432 veröffentlichen.
  • redis:8-alpine ersetzt ein einfaches redis, das zu latest aufgelöst wird. Im August 2026 ist das Redis 8. Der Pin hält dadurch die von Ihnen getestete Hauptversion fest und verhindert, dass während eines unabhängigen docker compose pull unerwartet Redis 9 installiert wird.
  • pgvector/pgvector:pg16 bleibt genau so gesetzt wie beim Upstream-Anbieter. Der Grund dafür wurde oben genannt.

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 aktualisieren anschließend DATABASE_URL entsprechend.

Konfiguration in config/config.json

AFFiNE liest seine Einstellungen aus config/config.json. Dabei handelt es sich um das Verzeichnis, das Sie unter /root/.affine/config eingebunden haben. Diese Datei wird nicht automatisch erstellt. Sie müssen sie daher vor dem ersten Start anlegen. Öffnen Sie ~/affine/config/config.json in einem Editor und tragen Sie diesen Inhalt ein. Ersetzen Sie das Beispiel 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 aus diesem Wert Freigabelinks und Einladungen zu Arbeitsbereichen. Wenn dort noch http://localhost:3010 steht, verweist eine von Ihnen gesendete Einladung auf den Rechner des Empfängers und schlägt dort fehl. Setzen Sie den Wert vor dem ersten Start auf die öffentliche HTTPS-Adresse. Dadurch stimmen die Datei und das Admin-Panel immer überein. Einstellungen, die Sie später im Admin-Panel ändern, werden in der Datenbank gespeichert und nicht in diese Datei zurückgeschrieben.

copilot steuert die KI-Funktionen. copilot.byok.enabled aktiviert die Verwendung eigener Schlüssel. Damit kann der Besitzer eines Arbeitsbereichs den Schlüssel seines eigenen Modellanbieters in den Einstellungen des Arbeitsbereichs eintragen. Das Self-Hosting von AFFiNE umfasst kein KI-Abonnement. Lassen Sie beide Werte auf false, wenn Sie diese Funktion nicht benötigen.

Starten Sie den Stack:

docker compose up -d
docker compose ps --all

docker compose ps --all sollte affine_postgres und affine_redis als healthy, affine_server als running und affine_migration_job mit dem Status exited (0) anzeigen. Der Parameter --all ist wichtig: Ohne ihn zeigt docker compose ps nur laufende Container an. Ein Migrationsjob, der erfolgreich abgeschlossen wurde, erscheint dann überhaupt nicht. Jeder andere Exit-Code des Migrationsjobs muss untersucht werden. 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.

Admin-Konto 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 der Server noch keinen Administrator hat. In diesem Ablauf gibt es keinen Einladungscode und kein Setup-Token. Die erste Person, die die Registrierung abschließt, wird Administrator Ihres Servers. Deshalb muss der Port geschlossen bleiben, bis Sie sich registriert haben.

Aus diesem Grund bindet die obige Compose-Datei 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 in Ihrem 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 bereitstellen. Das gleiche Wettrennen gibt es bei anderen selbst gehosteten Anwendungen. Es ist noch problematischer, wenn die erste Anmeldung einen an den Hostnamen gebundenen Passkey erstellt. 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 Postgres-Datenverzeichnis. Es enthält Dokumente, Benutzer, Arbeitsbereiche, Berechtigungen und alle Servereinstellungen, die im Administrationsbereich gespeichert wurden.
  • ./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 sowie private.key, die vom Migrationsjob erzeugt wurden.

Upstream verwendet hier Bind-Mounts statt benannter Volumes. Diese Entscheidung ist beabsichtigt: Sie können diese Pfade mit gewöhnlichen Befehlen als tar-Archiv packen 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 behandelt.

So sichern Sie AFFiNE

Zwei Dinge müssen gesichert werden, und sie werden auf unterschiedliche Weise gesichert. Die Datenbank ist ein laufender Server. Wenn Sie ihre Dateien während des Betriebs kopieren, erhalten Sie eine beschädigte Kopie. 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 dessen lokalen Socket ausgeführt. Daher werden Sie nicht nach dem Passwort gefragt. Prüfen Sie die Größe in der Ausgabe von ls. Eine Datei mit wenigen hundert Bytes bedeutet, dass der Dump fehlgeschlagen ist, während die Shell die Datei trotzdem erstellt hat. Diesen Fehler entdeckt 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
tar czf backup/config-$(date +%F).tgz config docker-compose.yml .env

Sichern Sie das vollständige Verzeichnis config, die Compose-Datei und .env, da der Dump diese Dateien nicht enthält. Der Migrationsjob erstellt außerdem config/private.key. Wenn Sie nur config.json kopieren, fehlt diese Datei. In den Einstellungen des Adminbereichs gespeicherte Konfigurationen liegen in der Datenbank. Der oben erstellte Dump enthält sie daher bereits. Seit dem 12. Oktober 2026 können geänderte Servereinstellungen laut der Backup-Seite von AFFiNE außerdem aus dem Adminbereich exportiert werden. Kopieren Sie alle drei Dateien vom Server. Eine Sicherung auf demselben Datenträger wie die geschützten Daten ist keine Sicherung. Diese Aufteilung aus einem Datenbank-Dump und einem tar-Archiv des Upload-Verzeichnisses sollten Sie für jeden anderen zustandsbehafteten Container übernehmen. 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 Wiederherstellungsschritte, bevor Sie sie benötigen, und lesen Sie sie genau. In der im August 2026 veröffentlichten Fassung wurde eine Datei namens affine.backup in den Container kopiert und anschließend aus ./pg.backup wiederhergestellt. Das sind zwei verschiedene Namen. Außerdem wurde ein Verzeichnis ./postgres entfernt, während die aktuelle Compose-Datei ihre Daten in ./data/postgres speichert. Seit dem 12. Oktober 2026 verwendet die Seite durchgehend affine.backup und weist darauf hin, das alte Postgres-Verzeichnis aufzubewahren, bis die Wiederherstellung überprüft wurde. Die Falle ist im Original damit behoben. Eine zuvor gespeicherte Kopie der Schritte enthält sie jedoch weiterhin. Verwenden Sie in jedem Fall die Pfade, die Sie tatsächlich eingesetzt haben, und nicht die Pfade aus einem beliebigen Snippet. Dies ist die Abfolge für das Layout in diesem Leitfaden:

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 und nicht ein rm. Wenn Sie eine Datenbank überschreiben, von der Sie keine Kopie aufbewahrt haben, kann aus einem falschen Befehl ein vollständiger Datenverlust werden. 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 beschädigten 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 in einem 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-Hosting-Build nicht enthält

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

Echtzeit-Zusammenarbeit ist vorhanden. Sie ist das zentrale Feature, auf das sich alle Hinweise zur Dimensionierung beziehen, da die Dokumentation von AFFiNE den Speicherbedarf dem Synchronisierungssystem und dem Zusammenführen von Dokumenten zuschreibt. Offline-Bearbeitung ist für viele der Grund, ein Local-First-Tool einzusetzen. Die Desktop-Anwendung kann Ihren Self-Hosting-Server zur Workspace-Liste hinzufügen und sich dort anmelden. Testen Sie das genaue Offline-Verhalten, von dem Ihr Team abhängt, 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 ist das die Änderung, durch die Sie die Grenze überschreiten. Die Suche innerhalb des Clients funktioniert weiterhin für den geöffneten Workspace.

Vor der Einladung weiterer Personen sollten Sie zwei Grenzen kennen. Ein Self-Hosting-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-Hosting-Instanzen werden in der Dokumentation als vorgesehen, aber noch nicht vollständig implementiert beschrieben. Diese Angabe wurde im Oktober 2026 geprüft. Für einen Haushalt oder ein kleines Team ist keine dieser Grenzen relevant. Beide sind relevant, wenn Sie vierzig Personen umstellen möchten.

Upgrades

Lesen Sie zuerst die Release Notes, besonders bei einem Minor-Versionssprung wie von 0.26 auf 0.27, bei dem inkompatible Änderungen eingeführt werden. Sichern Sie die Datenbank und das Storage-Verzeichnis, bevor Sie Änderungen vornehmen, da der Migrationsjob beim nächsten Start das Schema ändert und diese Änderung nicht rückgängig gemacht werden kann. Ä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 sauber beendet wird. docker image prune entfernt anschließend die alten Layer. Ein historischer Hinweis für Installationen mit einer sehr alten Version: Ab Version 0.23.0 wurde der Image-Name von affine-graphql in affine geändert. Eine ältere Compose-Datei muss daher vor dem Pull in ihren Image-Zeilen angepasst werden, damit ein Image gefunden wird. Ab Version 0.27.4 wurden in der offiziellen Compose-Vorlage die Pfade für Postgres, Uploads und Konfiguration geändert. Außerdem wurde die Standardverbindung zur Datenbank geändert. Vergleichen Sie die neue Datei daher mit Ihrer vorhandenen Datei, statt sie über eine funktionierende Installation zu kopieren.

FAQ

Warum startet der AFFiNE-Container nie?

Der Dienst affine deklariert condition: service_completed_successfully für den Job affine_migration. Wenn die Migration mit einem anderen Status als 0 beendet wird, startet der Server daher nie, und es erscheint überhaupt keine Weboberfläche. Führen Sie docker compose logs affine_migration aus, um zu sehen, bei welchem Schritt der Ablauf beendet wurde. Prüfen Sie, ob eine manuell bearbeitete Compose-Datei weiterhin pgvector/pgvector:pg16 verwendet: Das Standard-Postgres enthält nicht die pgvector-Erweiterung, die das AFFiNE-Schema weiterhin deklariert. Das gilt auch dann, wenn v0.27.4 die vier in v0.27.3 vorhandenen Embedding-Modelle nicht mehr definiert.

Wie viel RAM benötigt selbst gehostetes AFFiNE?

Die Anforderungsseite von AFFiNE nennt mindestens 4 CPU-Kerne und 2 GB RAM. 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 kurzfristig bis zu 1 GB belegen kann. Auf einem Server mit 2 GB ist diese Spitze das Problem, nicht die Leerlauflast: Der Out-of-Memory-Killer des Kernels beendet einen Prozess, höchstwahrscheinlich den AFFiNE-Server als größten Prozess. Anschließend startet restart: unless-stopped ihn erneut, sodass Benutzer statt einer Fehlermeldung lediglich ein erneutes Laden der Seite sehen. Bestätigen 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 als Puffer gegen kurze Spitzen an. Wenn die Prozesse weiterhin beendet werden, erhöhen Sie den Arbeitsspeicher auf 4 GB.

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 sowie ./config für config.json und die erzeugten private.key. Sichern Sie die Datenbank mit docker compose exec -T postgres pg_dump --format c --username affine affine > affine.dump, anstatt die Dateien zu kopieren, da eine laufende Postgres-Datenbank nicht sicher kopiert werden kann. Der Dump enthält auch die im Administrationsbereich gespeicherten Einstellungen, weil AFFiNE diese in der Datenbank ablegt. Packen Sie ./data/storage mit Tar und bewahren Sie das gesamte Verzeichnis config, die Compose-Datei und .env auf. Diese Dateien gehören nicht zur Datenbank.

Funktioniert die Zusammenarbeit in Echtzeit mit einem selbst gehosteten AFFiNE?

Ja. Dafür muss nichts aktiviert werden. Die einzige Voraussetzung ist Ihr Reverse Proxy, da die Synchronisierung über WebSocket-Verbindungen läuft. Bei nginx bedeutet dies 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 der Verbindungen durchführt, wird der Arbeitsbereich normal geladen und die Anmeldung funktioniert. Änderungen, die in einem Browser vorgenommen werden, erscheinen dann jedoch nicht in einem anderen.

Kann ich AFFiNE mit einem Standard-Postgres-Image betreiben?

Verwenden Sie pgvector/pgvector:pg16. Dieses Image enthält Postgres 16 mit einkompilierter pgvector-Erweiterung. AFFiNE v0.27.4 deklariert in schema.prisma weiterhin extensions = [pgvector(map: "vector")], definiert jedoch die vier Modelle mit vector(1024)-Embedding-Spalten aus v0.27.3 nicht mehr. Standard-Postgres enthält die deklarierte Erweiterung nicht. Wenn Sie AFFiNE stattdessen mit einem externen Postgres-Server verbinden, installieren Sie pgvector dort und erstellen Sie die Erweiterung in der Zieldatenbank, bevor Sie die Migration ausführen.

War dieser Artikel hilfreich?