AFFiNE selbst hosten, eine Open-Source-Notion-Alternative
Folge dem auf deinem eigenen Server. Kostenlos starten, keine Karte nötig
AFFiNE ist ein Open-Source-Workspace, der einen Notion-artigen Dokumenteneditor, ein unendliches Whiteboard und eine leichtgewichtige Datenbank in einer App vereint, wobei deine Inhalte auf einem Server bleiben, den du kontrollierst, statt in der Cloud eines Anbieters. Es ist auch ein wirklich nicht-trivialer Self-Host: eine Postgres-Datenbank mit der pgvector-Erweiterung, Redis, und ein einmaliger Migrationsjob, der abgeschlossen sein muss, bevor die App selbst starten darf. Am Ende dieses Guides läuft das alles auf deinem eigenen Server, hinter HTTPS, mit Backups und einem wiederholbaren Upgrade.
Was du brauchst
- Einen Server mit Docker und dem Compose-Plugin. Ein Hetzner CX23 (2 vCPU, 4 GB RAM, 40 GB NVMe, 5,99 € im Monat exkl. MwSt.) betreibt AFFiNE bequem für einen persönlichen Workspace oder ein kleines Team.
- Eine Domain oder Subdomain, die auf die IP-Adresse des Servers zeigt.
- SSH-Zugang zum Server.
- Etwa zehn Minuten, die meiste Zeit davon Warten auf DNS und den ersten Migrationslauf.
Schritt 1: DNS auf den Server zeigen und die Firewall öffnen
Erstelle einen A-Record für deine Subdomain, der auf die öffentliche IP des Servers zeigt, und sperre den Host dann auf genau das, was du brauchst:
ufw allow OpenSSH
ufw allow 80
ufw allow 443
ufw --force enable
Schritt 2: Ein Postgres-Passwort generieren
openssl rand -hex 24
mkdir -p /opt/affine
cat > /opt/affine/.env <<'EOF'
POSTGRES_PASSWORD=paste-the-generated-value-here
EOF
chmod 600 /opt/affine/.env
Schritt 3: Die Compose-Datei schreiben
Das ist der Teil, den die meisten Self-Host-Guides für AFFiNE falsch machen: Die offizielle Compose-Datei führt die Datenbankmigration als eigenen kurzlebigen Service aus, nicht als Schritt innerhalb des Starts der Haupt-App, und der App-Container muss warten, bis dieser Job tatsächlich fertig ist, nicht nur gestartet, bevor er hochfährt.
services:
affine:
image: ghcr.io/toeverything/affine:0.26.7
restart: unless-stopped
depends_on:
redis:
condition: service_healthy
db:
condition: service_healthy
affine-migration:
condition: service_completed_successfully
environment:
DATABASE_URL: postgresql://affine:${POSTGRES_PASSWORD}@db:5432/affine
REDIS_SERVER_HOST: redis
AFFINE_SERVER_EXTERNAL_URL: https://notes.yourdomain.com
AFFINE_INDEXER_ENABLED: "false"
volumes:
- affine_storage:/root/.affine/storage
- affine_config:/root/.affine/config
expose:
- "3010"
affine-migration:
image: ghcr.io/toeverything/affine:0.26.7
command: ["node", "./scripts/self-host-predeploy.js"]
environment:
DATABASE_URL: postgresql://affine:${POSTGRES_PASSWORD}@db:5432/affine
REDIS_SERVER_HOST: redis
AFFINE_INDEXER_ENABLED: "false"
volumes:
- affine_storage:/root/.affine/storage
- affine_config:/root/.affine/config
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
db:
image: pgvector/pgvector:0.8.7-pg16
restart: unless-stopped
environment:
POSTGRES_USER: affine
POSTGRES_DB: affine
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- affine_db_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U affine -d affine"]
interval: 10s
timeout: 5s
retries: 5
redis:
image: redis:7.2.5-alpine
restart: unless-stopped
healthcheck:
test: ["CMD", "redis-cli", "--raw", "incr", "ping"]
interval: 10s
timeout: 5s
retries: 5
volumes:
- affine_redis_data:/data
caddy:
image: caddy:2
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
- caddy_data:/data
- caddy_config:/config
depends_on:
- affine
volumes:
affine_storage:
affine_config:
affine_db_data:
affine_redis_data:
caddy_data:
caddy_config:
Dieser Guide pinnt 0.26.7, das aktuelle stabile Release zum Zeitpunkt des Schreibens, für jeden AFFiNE-Container; pgvector/pgvector:0.8.7-pg16 ist Postgres 16 mit der pgvector-Erweiterung, die AFFiNE für seine eigenen Embeddings braucht. affine-config ist bewusst ein eigenes, von affine-storage separates Volume: AFFiNEs Predeploy-Skript generiert beim ersten Lauf ein echtes, PEM-formatiertes Signing- und Encryption-Keypaar in /root/.affine/config/private.key, kein einfaches Secret, das du selbst setzt, also würde ohne ein persistentes Volume für dieses Verzeichnis jedes Redeploy still ein neues erzeugen und das letzte ungültig machen. AFFINE_INDEXER_ENABLED bleibt hier false, passend zu AFFiNEs eigenem Standard; schalte es ein, sobald du Volltextsuche über Dokumente willst, es ist nicht nötig, um die App zu nutzen.
Prüfe, dass die Datei sauber parst:
docker compose config
Schritt 4: Caddy hinzufügen und den Stack starten
notes.yourdomain.com {
reverse_proxy affine:3010
}
docker compose up -d
docker compose up startet zuerst db und redis, wartet auf beide Healthchecks, und führt dann affine-migration bis zum Abschluss aus, bevor affine selbst starten darf. Schau es dir an:
docker compose logs -f affine-migration
Du solltest eine Migrationsausgabe sehen, die mit einem sauberen Exit endet, dann:
docker compose ps
curl -I https://notes.yourdomain.com
Du solltest affine-migration als Exited (0) gelistet sehen, jeden anderen Service Up oder healthy, und der curl-Aufruf sollte HTTP/2 200 zurückgeben.
Schritt 5: Deinen Workspace erstellen
Besuche https://notes.yourdomain.com in einem Browser. Nichts ist vorgefüllt, also erstelle deinen ersten Account und Workspace direkt über die Web-UI. Von dort aus lade Teammitglieder ein und beginne, Dokumente oder Whiteboards zu schreiben, genau wie du es in einem gehosteten Notion oder Miro tun würdest.
Schritt 6: Postgres und das Config-Volume sichern
Dokumente und Whiteboard-Inhalte liegen in Postgres; das Signing-Keypaar aus Schritt 3 liegt im affine-config-Volume. Sichere beides:
cat > /opt/affine/backup.sh <<'EOF'
#!/bin/sh
cd /opt/affine
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
docker compose exec -T db pg_dump -U affine affine | gzip > backups/db-$TIMESTAMP.sql.gz
docker run --rm -v affine_affine_config:/config -v "$PWD/backups":/backup alpine \
tar czf /backup/config-$TIMESTAMP.tar.gz -C /config .
find backups -mtime +14 -delete
EOF
mkdir -p /opt/affine/backups
chmod +x /opt/affine/backup.sh
(crontab -l 2>/dev/null; echo "0 3 * * * /opt/affine/backup.sh") | crontab -
Führe es einmal von Hand aus und prüfe, dass es funktioniert hat: ./backup.sh && ls -lh backups. Du solltest sowohl eine .sql.gz- als auch eine .tar.gz-Datei mit dem heutigen Zeitstempel sehen. Kopiere beide vom Server weg, das Config-Volume ohne Backup zu verlieren bedeutet, dass jeder Client beim nächsten Neustart einem neuen Signing-Key neu vertrauen muss, nicht nur Datenverlust.
Schritt 7: Upgrade
docker compose pull
docker compose up -d
Compose führt affine-migration erneut bis zum Abschluss aus, bevor affine selbst neu erstellt wird, dieselbe Abhängigkeitsreihenfolge wie in Schritt 4, sodass jede Schema-Änderung in der neuen Version automatisch angewendet wird. Prüfe docker compose logs -f affine-migration erneut auf einen sauberen Exit, bevor du annimmst, dass das Upgrade gelandet ist.
Die Abkürzung: mit Railyard
Alles oben, Postgres mit pgvector, Redis, der Migrationsjob, der abgeschlossen sein muss, bevor die App startet, das separate Config-Volume für den Signing-Key, ist auch das, was Railyards AFFiNE-Stack-Template für dich einrichtet. Verbinde einen Server, dann wähle AFFiNE im Templates-Katalog: app.railyard.run/deploy?stack=affine. Railyard verdrahtet die Connection-Strings für Datenbank und Redis automatisch und führt das Predeploy-Skript als Release-Schritt vor jedem Deploy aus, nicht nur dem ersten, und stellt das HTTPS-Zertifikat für deine Domain aus.
Siehe Deploy mit einem Button und Datenbanken und Backups dafür, wie Templates und persistente Volumes auf der Plattform allgemeiner funktionieren.
Troubleshooting
Der affine-Container startet in einer Schleife neu und liefert nie etwas aus. Prüfe docker compose ps, falls affine-migration etwas anderes als Exited (0) zeigt, verweigert die Haupt-App korrekt den Start auf schlechten Daten, statt gegen ein nicht migriertes Schema auszuliefern; lies docker compose logs affine-migration für den tatsächlichen Fehler.
Jeder Client wird nach einem Neustart ausgeloggt oder soll dem Server neu vertrauen. Das affine-config-Volume wurde verloren oder neu erstellt, was den Signing-Key neu generiert. Bestätige, dass das Volume in docker volume ls gelistet ist und nicht versehentlich bereinigt wurde; das Wiederherstellen des config-*.tar.gz-Backups aus Schritt 6 in ein frisches Volume ist der einzige Weg zurück.
HTTPS wird nicht ausgestellt. docker compose logs caddy mit wiederholten ACME-Fehlern bedeutet fast immer, dass sich der DNS-Record aus Schritt 1 noch nicht verbreitet hat oder auf die falsche IP zeigt; bestätige mit dig +short notes.yourdomain.com.
Postgres-Verbindung verweigert. Bestätige, dass DATABASE_URL sowohl bei affine als auch bei affine-migration db als Hostnamen nutzt, nicht localhost.
Die Suche liefert keine Ergebnisse über Dokumente. Das ist mit dem Standard-Setup erwartet; AFFINE_INDEXER_ENABLED ist standardmäßig false, passend zu AFFiNEs eigener Referenz-Compose-Datei, schalte es in Schritt 3 ein, falls du es brauchst.
FAQ
Kann ich das Config-Volume überspringen und selbst einfach einen statischen Key setzen? Nein, AFFiNEs eigenes Predeploy-Skript generiert das echte PEM-Keypaar, das es braucht; es gibt keine dokumentierte Umgebungsvariable, um direkt eins zu liefern, genau deshalb ist das Volume aus Schritt 3 notwendig, nicht optional.
Braucht AFFiNE einen externen E-Mail-Anbieter? Nicht, um die App im Alltag zu nutzen; E-Mail ist relevant, falls du Einladungs-E-Mails oder Passwort-Reset-Flows willst, die AFFiNE separat zu konfigurieren unterstützt, aber nicht nötig für eine funktionierende Single-Workspace-Installation sind.
Ist Echtzeit-Zusammenarbeit beim Bearbeiten enthalten? Ja, mehrere Personen, die gleichzeitig dasselbe Dokument oder Whiteboard bearbeiten, ist ein Kernfeature, kein Add-on, gestützt von demselben Postgres und Redis, das hier schon läuft.
Kann ich hochgeladene Dateien in Object Storage statt ins lokale Volume verschieben? AFFiNE unterstützt ein S3-kompatibles Storage-Backend für Anhänge; der Standard hier hält alles aus Einfachheitsgründen auf affine_storage, lohnt es sich zu überdenken, sobald Anhänge groß werden.
Was passiert, wenn ich den Server ohne Backup verliere? Dokumente, Whiteboards und der Signing-Key sind alle weg; es gibt keinen Wiederherstellungsweg zu Daten, die nirgendwo anders gesichert wurden, deshalb ist Schritt 6 in der Praxis nicht optional.
Eine funktionierende AFFiNE-Installation besteht aus vier Services, die in einer bestimmten Reihenfolge voneinander abhängen: zuerst Postgres und Redis, dann der Migrationsjob, zuletzt die App. Sobald diese Reihenfolge stimmt und Backups laufen, bleibt kaum noch etwas zu pflegen außer dem gelegentlichen Versionssprung aus Schritt 7.
Warum Leute sich dafür Railyard entscheiden
- Dein eigenes Cloud-Konto. Deploye auf DigitalOcean, Hetzner, Vultr, AWS, OVHcloud oder jedem Linux-Server per SSH, direkt von deinem Anbieter abgerechnet, ohne Aufschlag.
- Ein fester Preis pro Team. Free ist für immer kostenlos: ein Server plus ein Datenbankserver, drei Apps, keine Karte. Pro kostet 19 $ im Monat für bis zu fünf Server und unbegrenzte Apps. Team kostet 49 $ im Monat mit unbegrenzten Servern. Keine Kosten pro App, pro Deployment, pro Sitz oder pro Gigabyte.
- Templates mit einem Klick. Deploye Open-Source-Apps, wie Datenbanken, Analytics und Dev-Tools, mit einem Klick auf deinem eigenen Server.
- Verwaltete Datenbanken. Postgres, MySQL und Redis mit geplanten, wiederherstellungsgeprüften Backups und Point-in-Time-Recovery.
- Push-to-Deploy. Deploye von GitHub, GitLab oder der CLI, mit automatischem HTTPS und Releases ohne Ausfallzeit.