Der letzte Anlauf hat das Problem nicht geloest. Die Freigabe stand weiter in der Basisdatei, nur mit anderem Vorgabewert -- und Vorgabewerte greifen nicht, wo die Variable schon gesetzt ist: in jeder .env, die aus der bisherigen .env.example entstanden ist, steht POSTGRES_PORT="5432". Postgres band also unveraendert auf 0.0.0.0 und kollidierte mit anderen Stacks auf demselben Host; der Container kam gar nicht erst hoch. Das Overlay half nicht, weil es erst greift, wenn man es dazunimmt. Jetzt hat der db-Dienst keinen ports-Block mehr. Gebraucht wird er nur, wenn die Anwendung ausserhalb des Containers laeuft, und dafuer gibt es docker-compose.dev.yml (npm run docker:db). Damit ist der sichere Fall die Vorgabe und der Sonderfall die Ausnahme, nicht umgekehrt. Der App-Port bindet aus demselben Grund auf 127.0.0.1: Docker traegt seine Freigaben direkt in iptables ein und umgeht dabei ufw. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
13 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_SECRETspäter zu ändern macht gespeicherte Git-Tokens unlesbar. Sie müssen dann neu hinterlegt werden. Konten, Passwörter und Passkeys bleiben unberührt.
APP_URLspä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
portsanfügend zusammen. Das!resetdarin ist genau dafür da und braucht Compose 2.24 oder neuer.Das Trennzeichen in
COMPOSE_FILEist 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:
- Passkey hinzufügen — geht erst, wenn TLS steht
- 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_URLaufhttps://…, setzt Nebula dasSecure-Flag auf die Sitzungscookies. Läuft der Proxy aufhttps, dieAPP_URLaber noch aufhttp, 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
iptablesund geht dabei anufwvorbei. Eine Freigabe auf0.0.0.0steht also im Netz, auch wennufwsie zu melden scheint. Genau deshalb wird hier an127.0.0.1gebunden, 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
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.4.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
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-dynamicund ohneunsafe-inlinefür Skripte. - CSRF-Prüfung über
Sec-Fetch-Site/Originbei 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.
- Der Bildoptimierer von Next ist abgeschaltet: Nebula benutzt ihn nicht,
und
/_next/imagewäre sonst eine Angriffsfläche ohne Gegenwert.
Was du beisteuern musst
- TLS über den Reverse Proxy
APP_SECRETlang und zufällig- Datenbankport nicht nach außen öffnen
- Sicherungen wegschaffen
- Systemupdates:
unattended-upgradesfür das Betriebssystem,./scripts/update.shfü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.