Zwei Zugangswege, ein Server
Das Kernproblem: Joplin Desktop nutzt Node.js für HTTPS-Verbindungen und liest dabei
nicht den Windows-Zertifikatspeicher aus. Eine selbstsignierte CA — egal ob
mit mkcert erstellt und korrekt im System installiert — wird vom Sync-Client ignoriert.
Die Fehlermeldung lautet unable to verify the first certificate.
Die pragmatische Lösung: zwei getrennte Zugangswege. Der Sync-Client spricht direkt über HTTP auf Port 22300 mit dem Joplin Server. Der Browser nutzt die Web-UI über HTTPS via Nginx — dort funktioniert mkcert problemlos.
Joplin App │ HTTP Port 22300 ▼ FortiGate WAN │ VIP-Joplin │ → 10.0.1.83:22300 ▼ Joplin Server (direkt, kein Nginx)
Browser │ HTTPS Port 443 ▼ Pi-hole DNS │ joplin.local → FG-WAN ▼ FortiGate → Nginx │ VIP-HTTPS → Server1 │ proxy_pass → 10.0.1.83:22300 ▼ Joplin Server (via Nginx, SSL-terminiert)
Der Sync läuft ausschließlich im lokalen Heimnetz — niemals über das Internet. Alle Daten sind zusätzlich End-to-End-verschlüsselt durch Joplins eingebaute E2EE-Verschlüsselung. Selbst wenn jemand den HTTP-Traffic mitlesen könnte, sähe er nur verschlüsselte Blöcke. Das HTTP-Risiko ist im LAN-Kontext vertretbar.
Docker Compose auf Server3
Joplin Server läuft zusammen mit PostgreSQL als Datenbank auf Server3.
Wichtig: APP_BASE_URL muss exakt der URL entsprechen
die im Sync-Client eingetragen wird. Jede Abweichung führt zu
Invalid Origin-Fehlern beim Login.
services: joplin: image: joplin/server:latest container_name: joplin restart: unless-stopped ports: # Auf Server-IP binden, nicht 127.0.0.1 ! # Nginx auf Server1 muss von außen drauf zugreifen können - "10.0.1.83:22300:22300" environment: - APP_PORT=22300 # APP_BASE_URL = URL die der Sync-Client einträgt # Muss exakt übereinstimmen — kein trailing slash, kein https! - APP_BASE_URL=http://203.0.113.10:22300 - DB_CLIENT=pg - POSTGRES_PASSWORD=sicheres-passwort - POSTGRES_DATABASE=joplin - POSTGRES_USER=joplin - POSTGRES_PORT=5432 - POSTGRES_HOST=joplin-db depends_on: - joplin-db joplin-db: image: postgres:16 container_name: joplin-db restart: unless-stopped environment: - POSTGRES_PASSWORD=sicheres-passwort - POSTGRES_DATABASE=joplin - POSTGRES_USER=joplin volumes: - ./data/postgres:/var/lib/postgresql/data networks: default: name: joplin-net
mkdir -p /opt/joplin/data/postgres — dann
docker compose up -d. Beim ersten Start initialisiert PostgreSQL
die Datenbank, Joplin Server wartet automatisch.
https://joplin.local öffnen.
Standard-Login: admin@localhost / admin.
Sofort Passwort ändern und eigenen User anlegen.
http://203.0.113.10:22300 — HTTP, nicht HTTPS.
Dann Klick auf "Synchronisieren prüfen".
VIP & Firewall Policy für Sync
Der Sync-Client braucht einen direkten Weg zu Joplin Server — vorbei an Nginx. Dafür wird ein eigener VIP für Port 22300 angelegt, der direkt auf Server3 zeigt.
| Parameter | Wert |
|---|---|
| VIP-Name | VIP-Joplin |
| External IP | 203.0.113.10 (FortiGate WAN) |
| External Interface | wan ← nicht "wan1" |
| Mapped IP | 10.0.1.83 (Server3) |
| External Port | 22300 |
| Map to Port | 22300 |
# VIP anlegen config firewall vip edit "VIP-Joplin" set extip 203.0.113.10 set extintf "wan" set portforward enable set protocol tcp set extport 22300 set mappedip 10.0.1.83 set mappedport 22300 next end # Firewall Policy config firewall policy edit 0 set name "Joplin-Sync" set srcintf "wan" set dstintf "lan" set srcaddr "all" set dstaddr "VIP-Joplin" set action accept set service "TCP_22300" set nat disable next end
Web-UI unter joplin.local
Nginx auf Server1 proxied die Web-UI über HTTPS. Da Joplin Server intern auf HTTP läuft, terminiert Nginx hier SSL und leitet unverschlüsselt weiter — klassisches SSL-Offloading. Für den Browser ist alles grün, der Sync-Client weiß davon nichts.
server { listen 443 ssl; server_name joplin.local; ssl_certificate /etc/nginx/certs/homelab.pem; ssl_certificate_key /etc/nginx/certs/homelab-key.pem; location / { proxy_pass http://10.0.1.83:22300; proxy_http_version 1.1; 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; # WebSocket für Echtzeit-Updates proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; } }
joplin.local → 203.0.113.10 — wie alle anderen lokalen Domains
auf die FortiGate WAN-IP, nicht auf Server1 direkt.
FortiGate leitet via VIP-HTTPS auf Nginx, Nginx proxy_pass auf Server3.
Desktop, Mobile & Web-UI
| Client | Sync-URL | Protokoll | Zertifikat nötig? |
|---|---|---|---|
| Desktop (Windows/Mac/Linux) | http://203.0.113.10:22300 | HTTP | nein |
| Mobile (iOS / Android) | http://203.0.113.10:22300 | HTTP | nein |
| Browser (Web-UI) | https://joplin.local | HTTPS | mkcert CA installieren |
Da Sync über HTTP läuft, ist E2EE besonders empfehlenswert. In der Desktop-App unter Extras → Verschlüsselung → Aktivieren. Alle Notizen werden vor dem Sync verschlüsselt — der Server sieht nur Chiffrat, nie Klartext. Master-Passwort sicher aufbewahren — ohne es sind verschlüsselte Notizen unwiederbringlich verloren.
PostgreSQL automatisch sichern
Alle Notizen liegen in PostgreSQL. Ein täglicher Cron-Job auf Server3 sichert die Datenbank und räumt alte Backups automatisch auf.
# Backup-Verzeichnis anlegen (einmalig) # mkdir -p /opt/joplin/backups # Täglich um 03:00 Uhr 0 3 * * * docker exec joplin-db pg_dump -U joplin joplin \ > /opt/joplin/backups/joplin_$(date +\%Y\%m\%d).sql \ && find /opt/joplin/backups -name "*.sql" -mtime +14 -delete
cd /opt/joplin && docker compose pull && docker compose down && docker compose up -d —
PostgreSQL-Daten liegen im bind-mount ./data/postgres und bleiben beim Update erhalten.
Vorher ein manuelles Backup empfehlenswert.
Was schiefgehen kann
http:// umstellen, nicht https://.
APP_BASE_URL in der docker-compose.yml stimmt nicht mit der URL überein
die der Browser oder Client verwendet. Beide müssen exakt übereinstimmen —
inklusive Port, ohne trailing slash. Nach Änderung Container neu starten:
docker compose down && docker compose up -d.
https://joplin.local statt auf
http://203.0.113.10:22300. Die Web-UI läuft über Nginx + HTTPS,
der Sync-Endpunkt ist direkt Port 22300 über die FortiGate — zwei verschiedene Wege.
127.0.0.1:22300 gesetzt statt auf die
Server-IP. Nginx läuft auf Server1, Joplin auf Server3 — 127.0.0.1 hilft
dabei nicht. In der docker-compose.yml auf 10.0.1.83:22300:22300 ändern.