Files
nebula/DEPLOYMENT.md
florian.wilde 3617222cc1
CI / pruefen (pull_request) Canceled after 0s
docs: alle Markdown-Dokumente auf Stand v1.10.0
Die Dokumente waren unterschiedlich weit zurueck: FEATURES bei v1.9.0,
README bei v1.8.0, DEPLOYMENT und CLAUDE.md noch bei v1.5.0.

**README.** Die Einleitung nannte weder die projektuebergreifenden Aufgaben
noch Rueckblick, Zeitleiste oder die Issue-Anbindung. In der API-Tabelle
fehlten vier Routen (/api/activity, /api/tasks/:id/issue,
/api/repositories/:id/webhook und der eingehende /api/webhooks/:id); die
Suche war noch als "ueber die eigenen Notizen" beschrieben. Die
Projektstruktur zeigte ein lib/sync.ts, das laengst ein Verzeichnis ist, und
kannte weder tasks/ noch rueckblick/ oder verlauf/.

Unter "Getestet mit" stand Node 26.5.0, tatsaechlich laeuft 24.19.0. Die
Tabelle nennt jetzt den Werkzeugstand mit Datum und trennt ihn von der
Aussage, wann zuletzt gegen echte Git-Systeme geprueft wurde - das war im
Juli und ist seitdem nicht wiederholt worden.

**FEATURES.** Neues Kapitel 2c zu Rueckblick und Zeitleiste, samt der beiden
Entscheidungen, die sonst niemand nachvollziehen kann: Kalendertage in fester
Zeitzone statt "7 x 24 Stunden", und "erledigt" aus dem meta-Feld statt aus
dem Anzeigetext. Das Suchkapitel deckt jetzt Aufgaben mit ab.

**DEPLOYMENT.** Der Abschnitt "Updates" sprang bisher direkt ins Skript,
ohne zu erklaeren, was ein Update ueberhaupt ist - genau daran ist die Frage
"was meinst du mit Deploy?" entstanden. Jetzt steht vorneweg, dass Git und
laufende Instanz zwei Orte sind und woran man sieht, welche Fassung laeuft.
Dazu ein Abschnitt zu Webhooks (setzt Erreichbarkeit voraus, ohne sie geht
aber nichts verloren) und die Sitzungsfristen in der Haertungsliste.

**CLAUDE.md.** Sechs Fallstricke aus den letzten Runden, die sonst beim
naechsten Mal erneut Zeit kosten - allen voran "min-width: auto" bei Flex-
und Grid-Kindern, das hier zweimal zugeschlagen hat. Ausserdem: dass die
feste Zeitzone auch beim Rechnen gilt, dass Dialoge der Bildschirmtastatur
ausweichen muessen, dass ein Ausdrucksindex nur bei Zeichengleichheit
greift (mit EXPLAIN nachsehen), dass ein leeres Migrationsverzeichnis
"migrate diff" abbrechen laesst - und dass eine Ueberlaufmessung gegen
unvollstaendige Testdaten faelschlich gruen meldet.

Geprueft: Querverweise zwischen den Dokumenten aufgeloest, alle in README
und DEPLOYMENT genannten Umgebungsvariablen gegen Code und .env.example
gehalten, API-Tabelle gegen die tatsaechlich vorhandenen Routen.
2026-08-18 08:20:57 +02:00

15 KiB

Nebula auf einem Server betreiben

Schritt für Schritt: von der leeren Maschine bis zur laufenden Instanz hinter TLS — und wie Updates danach ablaufen.

Für Installation zum Ausprobieren und die Entwicklungsvarianten siehe README.md. Hier geht es um den Dauerbetrieb.


Was du brauchst

Server Linux mit Docker Engine ≥ 24 und Compose v2
Sonst git, curl, eine POSIX-Shell — kein Node, kein npm
Arbeitsspeicher 1 GB reichen; beim Bauen kurzzeitig ~2 GB
Plattenplatz ~2 GB für Abbilder, dazu die Datenbank
Domain Eine (Unter-)Domain, die auf den Server zeigt
TLS Pflicht, wenn du Passkeys willst — siehe Schritt 6

Warum TLS Pflicht ist: Passkeys verlangen einen sicheren Kontext. Über http:// an einer LAN-IP funktionieren sie nicht. Passwort und Zwei-Faktor gehen auch ohne, aber dann läuft dein Passwort im Klartext übers Netz — betreibe Nebula nicht ohne TLS.


1 · Docker installieren

curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker "$USER"
newgrp docker            # oder ab- und wieder anmelden

docker --version && docker compose version

2 · Nebula holen

sudo mkdir -p /opt/nebula && sudo chown "$USER" /opt/nebula
git clone https://git.example.com/dein-name/nebula.git /opt/nebula
cd /opt/nebula

3 · Konfigurieren

./scripts/setup.sh        # legt .env an, würfelt ein APP_SECRET, setzt Rechte 600

Das Skript ist reines POSIX-Shell und braucht kein Node — auf einem reinen Docker-Host ist keins installiert. Es nimmt openssl, falls vorhanden, sonst /dev/urandom.

Ganz von Hand geht auch:

cp .env.example .env
chmod 600 .env
openssl rand -base64 32        # den Wert als APP_SECRET eintragen

Dann die .env öffnen und mindestens diese drei Werte anpassen:

# Die Adresse, unter der du Nebula im Browser aufrufst.
# Bestimmt die WebAuthn-rpID — passt sie nicht, scheitern Passkeys.
APP_URL="https://nebula.deine-domain.de"

# Verschlüsselt die Git-Tokens. Lang, zufällig, niemals teilen.
APP_SECRET="…dein-langer-zufallswert…"

# Das Datenbankpasswort. Die Voreinstellung „nebula" ist für Entwicklung.
POSTGRES_PASSWORD="…noch-ein-langer-zufallswert…"

Die Ports müssen nicht angefasst werden: APP_PORT steht schon auf 127.0.0.1:3000, und die Datenbank veröffentlicht gar nichts.

APP_SECRET später zu ändern macht gespeicherte Git-Tokens unlesbar. Sie müssen dann neu hinterlegt werden. Konten, Passwörter und Passkeys bleiben unberührt.

APP_URL später zu ändern macht bestehende Passkeys ungültig, weil sie an die rpID gebunden sind. Passwörter funktionieren weiter.

Welche Ports überhaupt hinausgehen

Nach einer regulären Installation ist es genau einer:

Container Host-Port Wann
nebula-app APP_PORT, Vorgabe 127.0.0.1:3000 --profile full
nebula-db — nie
nebula-adminer ADMINER_PORT, Vorgabe 127.0.0.1:8080 nur --profile tools

Die Datenbank veröffentlicht nichts. Im Vollbetrieb spricht die Anwendung Postgres über das interne Docker-Netz als db:5432 an — die DATABASE_URL setzt Compose selbst. Auch scripts/backup.sh und restore.sh gehen über docker exec, nicht über den Host. Ein Host-Port wäre nur eine zusätzliche Angriffsfläche und würde mit anderen Postgres-Containern auf demselben Host um den Port streiten.

Wer die Datenbank beim Entwickeln doch vom Host aus braucht — npm run dev läuft ja außerhalb des Containers — nimmt docker-compose.dev.yml dazu (npm run docker:db). Auf einem Server hat diese Datei nichts zu suchen.

Auch der Port der Anwendung liegt auf 127.0.0.1: nach draußen geht es über den Reverse Proxy aus Schritt 6.

Variante: gar keinen Port veröffentlichen

Liegen mehrere Stacks auf demselben Docker-Host und regelt ein gemeinsamer Reverse Proxy den Zugang, braucht Nebula überhaupt keinen Host-Port. Dafür gibt es docker-compose.server.yml: es nimmt die letzte verbliebene Freigabe zurück und hängt die Anwendung in ein geteiltes Netz.

docker network create proxy      # falls der Proxy-Stack es nicht schon anlegt
# .env
COMPOSE_FILE="docker-compose.yml:docker-compose.server.yml"
COMPOSE_PROFILES="full"
PROXY_NETWORK="proxy"            # so heißt das Netz
PROXY_ALIAS="nebula"             # so findet der Proxy die Anwendung

Compose liest beides aus der .env. Danach genügt überall — auch in scripts/update.sh — ein schlichtes docker compose up -d. Im Proxy zeigt das Ziel dann nicht mehr auf 127.0.0.1:3000, sondern auf nebula:3000; Schritt 6 zeigt beide Schreibweisen.

Ohne die Datei würde ein Override die Portlisten zusammenhängen statt sie zu ersetzen — Compose führt ports anfügend zusammen. Das !reset darin ist genau dafür da und braucht Compose 2.24 oder neuer.

Das Trennzeichen in COMPOSE_FILE ist unter Linux der Doppelpunkt, unter Windows das Semikolon.

4 · Starten

docker compose --profile full up -d --build

Der erste Bau dauert ein paar Minuten. Der Container spielt beim Start selbst die Migrationen ein und wartet dabei auf die Datenbank.

docker compose --profile full logs -f app     # mitlesen, Strg+C beendet nur das Lesen
curl -s localhost:3000/api/health             # {"status":"ok","database":"up",…}

Ohne veröffentlichten Port geht die Frage an den Container statt an den Host — das Abbild bringt einen HEALTHCHECK mit:

docker inspect --format '{{.State.Health.Status}}' nebula-app   # healthy

5 · Ersten Zugang anlegen

Ruf https://nebula.deine-domain.de/setup auf (nach Schritt 6) oder vorübergehend über einen SSH-Tunnel:

ssh -L 3000:127.0.0.1:3000 benutzer@server
# dann lokal http://localhost:3000/setup öffnen

Dort legst du das Administratorkonto an und wählst die Betriebsart. Danach ist /setup dauerhaft gesperrt.

Anschließend gleich unter Mein Konto:

  1. Passkey hinzufügen — geht erst, wenn TLS steht
  2. Zwei-Faktor einrichten und die Wiederherstellungscodes wegschreiben

6 · Reverse Proxy mit TLS

Nebula bringt keinen eigenen TLS-Abschluss mit. Beispiel mit Caddy, das Zertifikate von selbst besorgt und erneuert:

# /etc/caddy/Caddyfile
nebula.deine-domain.de {
    reverse_proxy 127.0.0.1:3000
}
sudo systemctl reload caddy

Das genügt. Caddy setzt X-Forwarded-* von selbst, holt das Zertifikat bei Let's Encrypt und leitet http auf https um.

Läuft der Proxy selbst als Container im geteilten Netz (die Variante ohne veröffentlichte Ports), lautet das Ziel stattdessen nebula:3000 — Docker löst den Alias auf:

nebula.deine-domain.de {
    reverse_proxy nebula:3000
}
Dasselbe mit nginx
server {
    listen 443 ssl http2;
    server_name nebula.deine-domain.de;

    ssl_certificate     /etc/letsencrypt/live/nebula.deine-domain.de/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/nebula.deine-domain.de/privkey.pem;

    location / {
        proxy_pass http://127.0.0.1:3000;
        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;
    }
}

Zertifikat mit certbot --nginx -d nebula.deine-domain.de.

Steht APP_URL auf https://…, setzt Nebula das Secure-Flag auf die Sitzungscookies. Läuft der Proxy auf https, die APP_URL aber noch auf http, funktioniert die Anmeldung — nur ohne dieses Flag. Also beides gleichziehen.

7 · Firewall

sudo ufw allow 22/tcp
sudo ufw allow 80,443/tcp
sudo ufw enable

Port 3000 bleibt zu, 5432 wird ohnehin nie veröffentlicht. Die Firewall ist hier der zweite Riegel, nicht der erste.

Docker schreibt seine Portfreigaben direkt in iptables und geht dabei an ufw vorbei. Eine Freigabe auf 0.0.0.0 steht also im Netz, auch wenn ufw sie zu melden scheint. Genau deshalb wird hier an 127.0.0.1 gebunden, statt sich auf die Firewall zu verlassen.

8 · Sicherungen automatisieren

scripts/backup.sh legt einen pg_dump unter backups/ ab. Täglich per Cron:

crontab -e
# Jede Nacht um 3:15 sichern, 30 Tage aufheben
15 3 * * * cd /opt/nebula && ./scripts/backup.sh >> /var/log/nebula-backup.log 2>&1
30 3 * * * find /opt/nebula/backups -name '*.sql.gz' -mtime +30 -delete

Zurückspielen:

./scripts/restore.sh backups/nebula-2026-07-27-03-15-00.sql.gz

Die Sicherungen liegen auf demselben Server. Das hilft gegen einen verpatzten Import, nicht gegen einen Plattenausfall. Zieh sie regelmäßig woandershin — rsync, restic, was du ohnehin nutzt.


Updates

Es sind zwei getrennte Orte, und das ist der Punkt, an dem es leicht durcheinandergeht:

Was dort liegt
Das Git-Repository Der Quellcode. Dorthin wird entwickelt.
Diese Instanz Die laufende Anwendung, die du im Browser benutzt.

Ein Commit im Git ändert am laufenden Nebula nichts. Der Server holt sich von allein nichts ab; er betreibt weiter die Fassung, die dort zuletzt gebaut wurde. Ein Update ist der Schritt dazwischen: holen, bauen, umschalten.

Woran du siehst, was gerade läuft: die Versionsnummer unten in der Fußzeile. Ein Klick darauf öffnet den Änderungsverlauf. Steht dort eine ältere Nummer als im Git, fehlen dir die neueren Funktionen — sie sind geschrieben, aber noch nicht auf diesem Server.

Dafür gibt es scripts/update.sh. Es macht alles in der richtigen Reihenfolge und bricht ab, sobald ein Schritt scheitert:

cd /opt/nebula
./scripts/update.sh

Was es tut:

Schritt
1 Prüfen docker, compose, .env da? Ist APP_SECRET gesetzt und nicht mehr der Entwicklungswert?
(alles ohne Node — das Skript liest die Version selbst aus der package.json)
2 Holen git pull --ff-only; zeigt die neuen Commits und fragt nach
3 Sichern pg_dump vor jeder Schemaänderung
4 Bauen Erst bauen, dann umschalten — ein gescheiterter Bau lässt die alte Fassung laufen
5 Starten Container neu; die Migrationen laufen im Entrypoint
6 Prüfen Bis zu zwei Minuten warten, bis der Container sich gesund meldet

Scheitert die Gesundheitsprüfung, zeigt das Skript die letzten Logzeilen und nennt den Weg zurück.

./scripts/update.sh --yes            # ohne Rückfrage, für Cron
./scripts/update.sh --ref v1.9.0     # auf eine bestimmte Fassung
./scripts/update.sh --no-pull        # nur neu bauen, ohne git pull

Wenn doch etwas schiefgeht

# Zurück auf den vorigen Stand (die Kennung nennt das Skript beim Abbruch)
git checkout <kennung>
./scripts/update.sh --no-pull --yes

# Nur wenn eine Migration die Daten beschädigt hat:
./scripts/restore.sh backups/<die-sicherung-von-vorhin>.sql.gz

Migrationen werden nicht zurückgerollt. Eine Migration, die eine Spalte entfernt, ist nicht umkehrbar — deshalb sichert das Skript vorher. Steht ein Update mit Datenmigration an, lies vorher den Änderungsverlauf (Versionsnummer in der Fußzeile).


Betrieb

Zustand ansehen

docker compose --profile full ps
docker compose --profile full logs -f app
curl -s localhost:3000/api/health

Neu starten

docker compose --profile full restart app

Speicher- und Plattenverbrauch

docker stats --no-stream nebula-app nebula-db
docker system df
docker image prune -f          # alte Abbilder nach mehreren Updates

Webhooks

Der Abgleich holt den Stand in seinem Takt. Wer ihn sofort haben will — ein Push ist durch, ein Issue wurde geschlossen —, trägt im Git-System einen Webhook ein. Nebula erzeugt URL und Geheimnis am Repository (Funksymbol im Git-Bereich der Projektseite), eingetragen wird der Haken von Hand.

Voraussetzung ist, dass dein Git-System diese Instanz erreicht. Hinter dem Reverse Proxy ist das gegeben; in einem abgeschotteten Netz nicht. Der Endpunkt liegt unter /api/webhooks/<repositoryId> und ist der einzige ohne Anmeldung — ausgewiesen wird sich über eine Signatur, die gegen das hinterlegte Geheimnis geprüft wird.

Ohne Webhook geht nichts verloren. Der Abgleich prüft den Zustand verknüpfter Issues ohnehin mit und hakt erledigte Aufgaben nach. Der Webhook macht es nur sofort statt beim nächsten Durchlauf.

Automatischer Abgleich

Standardmäßig aus. Einzuschalten unter Admin-Einstellungen → Automatischer Abgleich: Abstand, Repositories je Durchlauf, ob fertige Projekte ausgelassen werden. Der Zeitgeber läuft im Serverprozess selbst — kein zusätzlicher Container, kein Cron.

Wer lieber von außen taktet, lässt ihn aus und ruft POST /api/admin/sync/run auf.


Härtung, die schon eingebaut ist

Damit du weißt, worum du dich nicht kümmern musst:

  • Der Container läuft als unprivilegierter Benutzer (nextjs, UID 1001), nicht als root.
  • Content-Security-Policy mit Nonce, strict-dynamic und ohne unsafe-inline für Skripte.
  • CSRF-Prüfung über Sec-Fetch-Site/Origin bei jedem schreibenden Zugriff.
  • SSRF-Schutz bei allen ausgehenden Aufrufen: jede Adresse wird aufgelöst und gegen Loopback, Link-Local und Metadaten-Endpunkte geprüft, Weiterleitungen eingeschlossen.
  • Ratenbegrenzung auf Anmeldung, ausgehende Abrufe und schreibende Zugriffe; Kontosperre nach acht Fehlversuchen.
  • Git-Tokens liegen AES-256-GCM-verschlüsselt in der Datenbank und verlassen den Server nie — auch nicht im Export.
  • Sitzungen laufen nach 8 Stunden ohne Nutzung ab und spätestens nach 30 Tagen (SESSION_IDLE_HOURS, SESSION_TTL_DAYS). In der Datenbank liegt nur der Hash des Tokens; das Cookie ist HttpOnly, SameSite=Strict und bei HTTPS zusätzlich mit dem Präfix __Host-.
  • Passwörter werden mit scrypt nach der Empfehlung des OWASP Password Storage Cheat Sheet gehasht (N = 2^17). Rechnet mit rund einer halben Sekunde je Anmeldung — das ist gewollt.
  • Der Bildoptimierer von Next ist abgeschaltet: Nebula benutzt ihn nicht, und /_next/image wäre sonst eine Angriffsfläche ohne Gegenwert.

Was du beisteuern musst

  • TLS über den Reverse Proxy
  • APP_SECRET lang und zufällig
  • Datenbankport nicht nach außen öffnen
  • Sicherungen wegschaffen
  • Systemupdates: unattended-upgrades für das Betriebssystem, ./scripts/update.sh für Nebula

Mehrbenutzerbetrieb

Nebula kann beides. Im Einzelbetrieb gibt es genau ein Konto und keine Registrierung. Umschalten unter Admin-Einstellungen.

Wichtig zu wissen: Projekte sind zwischen Konten hart getrennt. Auch ein Administrator sieht fremde Projekte nicht — er verwaltet Konten, keine Inhalte. Wer gemeinsam an einem Projekt arbeiten will, ist mit Nebula falsch beraten; das ist bewusst nicht vorgesehen.

Die Selbstregistrierung ist standardmäßig aus. Lass sie aus, wenn die Instanz aus dem Internet erreichbar ist und du nicht willst, dass sich Fremde Konten anlegen.