SSD Nodes Learn 🎉 VPS ab $4.99/Monat
Anleitungen Matt ConnorVon Matt Connor

AFFiNE selbst hosten mit Docker Compose

AFFiNE auf einem VPS betreiben: vier Container, feste Image-Tags, Speicherorte, Backups und was 2 GB RAM beim Betrieb tatsächlich leisten.

Was Sie beim Self-Hosting von AFFiNE erhalten

Mit Self-Hosting von AFFiNE erhalten Sie einen Workspace im Stil von Notion auf einem Server, den Sie selbst verwalten. AFFiNE läuft dabei in vier Containern: der Anwendung, einem einmalig ausgeführten Migrationsjob, Postgres und Redis. Echtzeit-Zusammenarbeit ist enthalten. Ein selbst gehosteter Workspace unterstützt standardmäßig bis zu 10 Benutzer. Die Installation besteht aus einer Compose-Datei und einer JSON-Konfigurationsdatei. Berücksichtigt werden müssen die Image-Tags, das Festplattenlayout, die Speicherobergrenze und der vorgeschaltete Proxy.

AFFiNE kombiniert 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 betreiben möchten, lesen Sie zuerst den Vergleich selbst gehosteter Notion-Alternativen. Dieser Leitfaden setzt die Entscheidung voraus und beschreibt den ordnungsgemäßen Betrieb von AFFiNE, statt die Lösungen erneut zu vergleichen.

Alle Angaben wurden am 8. August 2026 anhand der Dokumentation zum Self-Hosting von AFFiNE 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.

Was die vier Container tatsächlich tun

affine enthält Server und Webclient in einem Image. Der Dienst lauscht auf Port 3010.

affine_migration ist ein einmalig ausgeführter 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 null 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 ergänzt Postgres um den Spaltentyp vector. Dieser numerische Typ wird zum Speichern von Embeddings verwendet, 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 den Healthcheck dieses Dienstes. Beachten Sie, dass die bereitgestellte Compose-Datei Redis keinen persistenten Speicher zuweist, also kein Volume. Nach einem docker compose down bleibt darin nichts erhalten. Daraus geht unmittelbar hervor, 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 ergibt sich 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 KI-Funktionen jemals aktivieren. Die Erweiterung muss daher bereits in der Datenbank vorhanden sein, bevor die Migration abgeschlossen werden kann. Wenn Sie stattdessen postgres:16 verwenden, fehlt die Erweiterung. Die Migration kann diese Spalten nicht erstellen, und der Server wartet auf einen Job, der bereits fehlgeschlagen ist.

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

Noch ein Hinweis zu diesem Tag: pg16 bedeutet Postgres 16. Eine Hauptversion von Postgres können Sie nicht einfach ändern. Wenn Sie sie in einem vorhandenen Datenverzeichnis auf 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. Ein Wechsel der Hauptversion erfordert einen Dump und eine Wiederherstellung in ein neues Datenverzeichnis.

Wie viel CPU und RAM benötigt eine selbst gehostete AFFiNE-Instanz?

Auf der Anforderungsseite von AFFiNE werden mindestens 4 CPU-Kerne und 2 GB RAM verlangt. Für Dokumente mit mehr als 10,000 Wörtern 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 auf 1 GB ansteigen.

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

Ihre Kollegin oder Ihr Kollege sieht 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 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 nicht ausgereicht hat und kein Softwarefehler gefunden wurde. Beheben Sie das Problem von beiden Seiten. Richten Sie zuerst Swap ein, damit ein Spitzenbedarf zu einer Verlangsamung statt zu einem Absturz führt:

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 jetzt eine gesamte Swap-Größ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 lässt sich wesentlich leichter vorhersagen. 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, was nahezu nichts ist. Tausend hochgeladene Dateien fügen 10 GB hinzu. Damit ist der wesentliche Faktor erfasst. Dies sind veröffentlichte Planungswerte und keine Messungen einer laufenden Instanz. Betrachten Sie sie daher als Größenordnung und nicht als Garantie. Entscheidend ist das Verhältnis: Ihre Datenbank bleibt klein, während Ihre Uploads den Speicherbedarf 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. Ein Detail sollten Sie kennen, bevor Sie sich darauf verlassen: Am 8. August 2026 liest die an Release 0.27.3 angehängte Datei ihre Pfade weiterhin aus einer .env-Datei und verwendet dafür ${UPLOAD_LOCATION}, ${CONFIG_LOCATION} und ${DB_DATA_LOCATION}, während die Referenzseite der Dokumentation ein neueres Layout zeigt, das alles unter ./data ablegt 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 automatisch 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 Geheimnisse 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 vom Upstream-Projekt bereitgestellten Datei. Für jeden gibt es einen Grund.

  • 127.0.0.1:3010:3010 veröffentlicht den Port nur auf der Loopback-Adresse. Dadurch kann nichts außerhalb des Servers AFFiNE erreichen, bis Sie festlegen, wie der Zugriff erfolgen soll. '3010:3010' aus der Upstream-Datei bindet dagegen an jede Schnittstelle. Bei den meisten VPS-Images gehört dazu auch die öffentliche Schnittstelle.
  • POSTGRES_HOST_AUTH_METHOD: trust entfällt. Stattdessen wird ein Passwort festgelegt. Die Vertrauensauthentifizierung 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 an dieses Netzwerk anschließen oder während der Fehlersuche Port 5432 veröffentlichen.
  • redis:8-alpine ersetzt ein nicht versioniertes redis, das zu latest aufgelöst wird. Im August 2026 ist das Redis 8. Durch den Pin bleibt die getestete Hauptversion festgelegt. Außerdem verhindert er, dass Redis 9 bei einem unabhängigen docker compose pull eingespielt wird.
  • pgvector/pgvector:pg16 bleibt aus dem oben genannten Grund genau so gesetzt wie im Upstream-Projekt.

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.

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 fügen Sie den folgenden 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 daraus Freigabelinks und Einladungen zu Arbeitsbereichen. Wenn dort weiterhin http://localhost:3010 steht, verweist eine von Ihnen gesendete Einladung auf den Computer des Empfängers und schlägt dort fehl. Setzen Sie den Wert vor dem ersten Start auf die öffentliche HTTPS-Adresse. So stimmen die Datei und das Administrationspanel immer überein.

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

Starten Sie den Stack:

docker compose up -d
docker compose ps

docker compose ps sollte affine_postgres und affine_redis als fehlerfrei, affine_server als laufend und affine_migration_job mit dem Status exited (0) anzeigen. Jeder andere Exit-Code beim Migrationsjob muss untersucht werden. Das zugehörige Log nennt den Schritt, bei dem der Vorgang angehalten wurde:

docker compose logs affine_migration

Image festlegen, bevor Sie es vergessen

stable ist ein veränderlicher Tag. Der Release-Workflow von AFFiNE verweist bei jedem stabilen Build auf mehrere Tags. 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 von Ihnen nicht gewählten Zeitpunkt gegen Ihre Datenbank aus. Legen Sie das exakt getestete Image fest:

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 es sich um dasselbe Image mit zwei Rollen handelt. Eine Abweichung bedeutet, dass die Datenbank auf ein Schema migriert wird, während sie mit einem anderen Schema bereitgestellt wird. Ein Upgrade ist dann eine bewusste Änderung statt einer Überraschung: Digest ändern, Backup erstellen, docker compose pull, docker compose up -d.

Erstellen Sie das Administratorkonto, bevor es jemand anderes tut

Öffnen Sie /admin auf einer frisch eingerichteten Instanz. AFFiNE leitet Sie dann zur Seite für die Kontoerstellung weiter, weil auf dem Server noch kein Administrator vorhanden ist. In diesem Ablauf gibt es keinen Einladungscode und kein Setup-Token. Die erste Person, die diese Seite lädt, wird Administrator Ihres Servers. Der Port muss daher geschlossen bleiben, bis Sie sich registriert haben.

Deshalb 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 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.

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 und Berechtigungen.
  • ./data/storage wird im Container unter /root/.affine/storage eingebunden und enthält jede hochgeladene Datei.
  • ./config wird unter /root/.affine/config eingebunden und enthält config.json.

Upstream verwendet hier Bind-Mounts statt benannter Volumes. Diese Entscheidung ist bewusst getroffen: 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. Sie werden unterschiedlich gesichert. Die Datenbank ist ein aktiver 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 dessen lokalen Socket ausgeführt. Daher wird 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 bemerken viele erst sechs Monate später. Auch -T ist wichtig: Ohne diese Option kann Compose ein Terminal zuweisen und den binären Datenstrom beschädigen.

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

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

Nehmen Sie config.json manuell in Ihre Sicherung auf. Die AFFiNE-Dokumentation führt den Konfigurationsexport aus dem Administrationsbereich bei der Prüfung im August 2026 weiterhin als nicht implementiert. Die Datei auf dem Datenträger ist daher die einzige Kopie Ihrer Einstellungen. Kopieren Sie alle drei Dateien vom Server auf ein anderes System. Eine Sicherung auf demselben Datenträger wie die geschützten Daten ist keine Sicherung.

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 eingesetzt haben, statt der Pfade aus dem Snippet. 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. Eine Wiederherstellung über einer Datenbank, von der Sie keine Kopie aufbewahrt haben, kann durch einen einzigen falschen Befehl zu einem vollständigen Datenverlust führen. Das Verschieben des alten Verzeichnisses 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 kein Backup.

AFFiNE hinter einem bereits betriebenen Proxy bereitstellen

AFFiNE verwendet WebSocket, und das ist nicht optional. Die Dokumentation formuliert es eindeutig: WebSocket bildet die Grundlage des Sync- und Kollaborationssystems von AFFiNE. Ein Proxy, der diese Verbindungen nicht aktualisiert, führt daher zu einem Workspace, in dem Änderungen unbemerkt nicht mehr synchronisiert werden. Die Seite wird geladen, und die Anmeldung funktioniert. Eine Änderung, die Sie in einem Browser vornehmen, erreicht den anderen Browser jedoch nicht. Öffnen Sie in den Entwicklertools Ihres Browsers die Registerkarte Network und filtern Sie nach WS. Eine Verbindung, die immer wieder geöffnet und geschlossen wird, weist darauf hin, dass der Proxy das Upgrade nicht weiterleitet.

Wenn Sie Traefik bereits für andere Container betreiben, binden Sie AFFiNE als normalen Dienst ein. Löschen Sie den Block ports: 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'

Fügen Sie am Ende der Datei neben services: Folgendes hinzu:

networks:
  proxy:
    external: true

Der Name des Certificate Resolvers muss mit dem Namen in Ihrer Traefik-Konfiguration ü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. Der Betrieb mehrerer Anwendungen hinter einer gemeinsamen Instanz wird unter einem einzelnen Traefik vor mehreren Anwendungen beschrieben.

Bei nginx müssen Sie das Upgrade ausdrücklich 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. nginx gibt den Status 413 zurück, 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 darüber im Klaren, bevor Sie ein Team umstellen.

Echtzeit-Zusammenarbeit ist vorhanden. Sie ist das Feature, auf das sich alle Hinweise zur Dimensionierung beziehen, da die eigene Dokumentation von AFFiNE den Speicherbedarf dem Synchronisierungssystem und dem Zusammenführen von Dokumenten zuschreibt. Offline-Bearbeitung ist der Grund, warum viele eine Local-First-Anwendung einsetzen möchten. Die Desktop-Anwendung kann Ihren selbst gehosteten Server zur Arbeitsbereichsliste 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. Dadurch kommt ein fünfter Dienst und zusätzlicher Speicherbedarf hinzu. Auf einem System mit 2 GB Arbeitsspeicher ist das die Änderung, durch die Sie die Grenze überschreiten. Die Suche innerhalb des Clients funktioniert weiterhin für den geöffneten Arbeitsbereich.

Vor der Einladung weiterer Personen sollten Sie zwei Einschränkungen kennen. Ein selbst gehosteter Arbeitsbereich kann höchstens 10 Sitze erhalten. Für mehr Sitze benötigen Sie eine Team-Lizenz von AFFiNE. Unbegrenzter Blob-Speicher und eine unbegrenzte Blob-Größe für selbst gehostete Instanzen werden in der Dokumentation als vorgesehen, aber noch nicht vollständig implementiert beschrieben; dieser Stand wurde im August 2026 geprüft. Für einen Haushalt oder ein kleines Team ist keine dieser Einschränkungen relevant. Beide sind relevant, wenn Sie vierzig Personen umstellen möchten.

Upgrades

Lesen Sie zuerst die Release Notes. Das gilt besonders für einen Versionssprung wie von 0.26 auf 0.27, bei dem inkompatible Änderungen eingeführt werden. Sichern Sie die Datenbank und das Speicherverzeichnis, bevor Sie Änderungen vornehmen. Der Migrationsjob ändert Ihr Schema beim nächsten Start. Diese Änderung kann nicht rückgängig gemacht werden. Ä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 Prozess sauber beendet wird. docker image prune entfernt anschließend die alten Layer. Ein historischer Hinweis für Installationen mit sehr alter 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 Abruf angepasst werden, damit das Image gefunden wird.

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 Vorgang angehalten wurde. Die häufigste Ursache in einer manuell bearbeiteten Compose-Datei ist ein Standard-Image postgres anstelle von pgvector/pgvector:pg16. Das AFFiNE-Schema deklariert die Erweiterung pgvector und erstellt Tabellen mit vector(1024)-Spalten, die plain Postgres nicht anlegen kann.

Wie viel RAM benötigt ein 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 kurzzeitig 1 GB RAM benötigen kann. Auf einem Server mit 2 GB ist dieser Spitzenbedarf das Problem, nicht die Leerlauflast. Der Out-of-Memory-Killer des Kernels beendet den AFFiNE-Prozess, und restart: unless-stopped startet ihn erneut. Für Benutzer sieht das wie ein erneutes Laden der Seite und nicht wie ein Fehler aus. Bestätigen Sie dies mit docker inspect affine_server --format '{{.State.OOMKilled}}' und sudo dmesg -T | grep -i 'out of memory'. Erstellen Sie anschließend eine 2-GB-Swap-Datei, damit eine Lastspitze das System verlangsamt statt den Prozess zu beenden.

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. Eine laufende Postgres-Datenbank lässt sich nicht sicher kopieren. Packen Sie ./data/storage mit tar und bewahren Sie eine Kopie von config.json manuell auf. Der Export der Konfiguration aus dem Administrationsbereich ist laut Dokumentation im August 2026 noch nicht implementiert.

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 erfolgt. 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 die Verbindungen nicht auf WebSocket hochstuft, lädt der Workspace normal und die Anmeldung funktioniert. Änderungen, die in einem Browser vorgenommen werden, erscheinen dann jedoch nie in einem anderen Browser.

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

Nein. schema.prisma von AFFiNE deklariert extensions = [pgvector(map: "vector")] und definiert vier Tabellen mit einer embedding-Spalte vom Typ vector(1024). Der Migrationsjob erstellt diese Tabellen auch dann, wenn die AI-Funktionen deaktiviert sind. Verwenden Sie pgvector/pgvector:pg16. Dabei handelt es sich um Postgres 16 mit bereits 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.