· DevOps · 10 Min. Lesezeit
Statische Sites ohne CI-Cloud deployen: meine Ein-Befehl-Pipeline
Eine eigene Deploy-Strecke für statische Seiten, ohne Netlify oder Vercel. Der vollständige Ablauf mit Prüfbefehl nach jedem Schritt, dazu sieben Stolpersteine, von denen der erste unfertige Arbeit veröffentlicht, ohne dass jemand es merkt.

Für statische Seiten braucht es keinen SaaS-Deploy. Ein Astro-Build ist am Ende ein Ordner mit fertigem HTML, und den kann man selbst ausliefern. Der Reiz liegt nicht im Sparen, sondern in der Nachvollziehbarkeit: Sie sehen jede Zeile, die den Weg von Ihrem Rechner auf den Server bestimmt, und es gibt kein fremdes System, das dazwischen entscheidet.
Der Preis ist, dass Sie ein paar Dinge selbst richtig machen müssen, die eine CI-Cloud im Hintergrund erledigt. Dieser Beitrag zeigt den vollständigen Ablauf mit einem Prüfbefehl nach jedem Schritt, danach sieben Stolpersteine im Schema Symptom, Ursache, Prüfung, Lösung. Der erste davon ist der gefährlichste, weil er unfertige Arbeit veröffentlicht, ohne dass es jemand bemerkt.
Wann sich welcher Weg lohnt
Es gibt drei sinnvolle Ausbaustufen, und die mittlere wird oft übersprungen.
| Eigene Pipeline | Zentraler Build-Host | CI-Cloud | |
|---|---|---|---|
| Build läuft auf | Ihrem Arbeitsrechner | einem eigenen Server | fremder Infrastruktur |
| Reproduzierbar über Rechner hinweg | nein | ja | ja |
| Quellcode verlässt Ihr Netz | nein | nein | ja |
| Vorschau je Branch | nein | mit Aufwand | eingebaut |
| Parallele Mitwirkende | schlecht | brauchbar | gut |
| Betriebsaufwand | sehr gering | gering | keiner |
Die eigene Pipeline passt für ein bis wenige Seiten, die eine Person pflegt. Genau darum geht es hier.
Ein zentraler Build-Host ist der Schritt danach und lohnt sich früher als gedacht. Sobald Sie an zwei Rechnern arbeiten, bauen beide mit unterschiedlichen Node-Versionen und unterschiedlich altem node_modules. Was dabei herauskommt, unterscheidet sich. Am Ende dieses Beitrags steht, wie ich das gelöst habe.
Eine CI-Cloud wird richtig, sobald mehrere Leute parallel committen, Vorschau-Deploys je Branch gebraucht werden oder Tests im Build laufen sollen. Das ist kein Widerspruch zu eigenem Hosting, die Auslieferung kann weiterhin auf Ihren Server gehen.
Voraussetzungen prüfen
Node-Version. Astro schreibt eine Mindestversion vor, und sie steigt mit den Hauptversionen. Aktuell ist Astro 7.1. Was Ihr Projekt verlangt, steht im Projekt selbst:
node -v
grep -A2 '"engines"' package.jsonStimmen die nicht überein, bauen Sie mit einer Version, die niemand getestet hat.
Zugang zum Zielserver. Der Deploy braucht einen SSH-Schlüssel, der genau das darf und nicht mehr:
ssh -o BatchMode=yes -p 22 deploy@lab.rz.jabi-it.de 'echo ok'BatchMode=yes ist wichtig, weil der Befehl damit sofort scheitert statt nach einem Passwort zu fragen. Ein Deploy-Skript, das auf eine Passwortabfrage wartet, hängt sonst still.
Sauberer Arbeitsbaum. Dazu gleich mehr unter Stolperstein 1, aber die Prüfung gehört an den Anfang:
git status --porcelainDer Ablauf in sechs Schritten
Schritt 1: In einen Wegwerf-Ordner spiegeln
Gebaut wird nie im Arbeitsverzeichnis und schon gar nicht auf einem gemounteten Netzlaufwerk. Auf einem SMB-Share ist ein npm-Build quälend langsam und bricht an Dateirechten ab. Ein lokaler Temp-Ordner mit automatischem Aufräumen löst beides:
BUILD_DIR="$(mktemp -d "${TMPDIR:-/tmp}/site-build.XXXXXX")"
trap 'rm -rf "$BUILD_DIR"' EXITDer trap greift auch bei Abbruch mit Strg-C und lässt keine Datenleiche zurück.
Schritt 2: Abhängigkeiten installieren
cd "$BUILD_DIR" && npm cinpm ci statt npm install. Der Unterschied ist nicht kosmetisch: npm ci verlangt eine vorhandene package-lock.json, entfernt ein vorhandenes node_modules vorher vollständig und bricht mit einem Fehler ab, wenn Lockfile und package.json nicht zusammenpassen, statt das Lockfile stillschweigend zu aktualisieren. Das steht so in der npm-Doku zu npm ci. Genau dieses Verhalten wollen Sie im Deploy: lieber ein Abbruch als ein Build aus Abhängigkeiten, die niemand geprüft hat.
Schritt 3: Bauen
npm run buildAstro legt das Ergebnis standardmäßig in dist/ ab, veränderbar über die Option outDir. Das ist auch der Ordner, den die Astro-Doku zum Deployment als Publish-Verzeichnis für eigene Hosts nennt.
Prüfung: Der Build meldet Erfolg, aber verlassen Sie sich nicht darauf. Sehen Sie nach, ob tatsächlich etwas entstanden ist:
find dist -type f | wc -l && du -sh distEine Zahl in der Größenordnung Ihrer Seitenzahl ist plausibel. Eine einstellige Zahl bedeutet, dass der Build zwar durchlief, aber fast nichts erzeugt hat.
Schritt 4: Packen und übertragen
COPYFILE_DISABLE=1 tar --no-xattrs -C dist -czf site.tgz .
scp site.tgz deploy@lab.rz.jabi-it.de:/tmp/Prüfung, bevor Sie etwas hochladen:
tar -tzf site.tgz | head -20
tar -tzf site.tgz | grep -cE '^\./\._|\.DS_Store' Der zweite Befehl muss 0 liefern. Warum, steht unter Stolperstein 4.
Schritt 5: Serverseitig umschalten
ssh deploy@lab.rz.jabi-it.de 'bash -s' <<'EOF'
set -e
cd /home/deploy/site-root
rm -rf site.new && mkdir -p site.new site
tar -xzf /tmp/site.tgz -C site.new
find site -mindepth 1 -delete
cp -a site.new/. site/
find site -type d -exec chmod 755 {} +
find site -type f -exec chmod 644 {} +
rm -rf site.new /tmp/site.tgz
EOFEntscheidend ist, dass site/ selbst nie gelöscht wird, sondern nur sein Inhalt. Warum, steht unter Stolperstein 2.
Schritt 6: Verifizieren
curl -s -o /dev/null -w '%{http_code}\n' https://lab.rz.jabi-it.de/Bei allem außer 200 bricht das Skript mit einem Fehlercode ab. Ein Deploy, der sich nicht selbst prüft, ist kein Deploy, sondern eine Hoffnung.
Die sieben Stolpersteine
1. Das Skript veröffentlicht Ihren gesamten Arbeitsbaum
Symptom: Auf der Live-Seite taucht eine Seite auf, die noch gar nicht fertig war. Oder eine Testdatei. Oder ein Entwurf, der nie jemand sehen sollte.
Ursache: Das Skript spiegelt in Schritt 1 den Ordner, nicht den Git-Stand. Ein rsync des Arbeitsverzeichnisses nimmt uncommittete Änderungen und untracked Dateien mit. Git kommt in der ganzen Kette nicht vor. Wer an Feature A und Feature B gleichzeitig arbeitet und nur A veröffentlichen will, veröffentlicht beides.
Prüfung: Vor jedem Deploy, und zwar automatisiert im Skript:
git status --porcelainLösung: Zwei Wege. Der einfache ist, den Deploy abzubrechen, wenn der Arbeitsbaum nicht sauber ist. Der flexible nutzt einen Wegwerf-Arbeitsbaum aus dem Git-Stand, in den Sie gezielt nur die gewünschten Dateien kopieren:
git worktree add --detach "$BUILD_DIR" HEAD
# gezielt nur die gewuenschten Aenderungen hineinkopieren
# ... bauen und deployen ...
git worktree remove --force "$BUILD_DIR"Der zweite Weg ist mehr Aufwand, aber er macht selektive Veröffentlichung überhaupt erst möglich. Wer aus dem Git-Stand baut, kann unfertige Arbeit strukturell nicht mehr versehentlich ausliefern.
2. nginx liefert weiter den alten Stand
Symptom: Der Deploy meldet Erfolg, die Dateien auf dem Server sind neu, der Browser bekommt trotzdem die alte Seite. Auch nach hartem Neuladen.
Ursache: Der Zielordner wurde gelöscht und neu angelegt. Bei einem Bind-Mount in einen Container hängt der Mount am Inode des ursprünglichen Ordners. Der neue Ordner hat eine neue Inode, der Container zeigt weiter auf die alte.
Prüfung: Vergleichen Sie, was der Container sieht, mit dem, was auf dem Host liegt:
docker compose exec web ls -la /usr/share/nginx/html | head
ls -la /home/deploy/site-root/site | headLösung: Den Ordner nie ersetzen, nur seinen Inhalt, so wie in Schritt 5 gezeigt. find site -mindepth 1 -delete leert ihn, cp -a füllt ihn neu. Der Inode bleibt, der Container merkt nichts, und die Umschaltung läuft ohne Neustart und ohne Ausfall.
3. HTTP 403 auf Bilder und Assets
Symptom: Die Seite lädt, aber Bilder, Schriften und alles aus public/ fehlen. nginx antwortet darauf mit 403, nicht mit 404.
Ursache: Die Dateien kommen mit zu strengen Rechten an, typischerweise 700 auf Verzeichnissen. Der nginx-Prozess im Container läuft unter einer anderen Benutzerkennung und darf nicht lesen.
Prüfung:
find /home/deploy/site-root/site -type d ! -perm -755 | headLösung: Rechte nach dem Entpacken normalisieren, wie in Schritt 5. Das ist idempotent und schadet nicht, wenn die Rechte schon stimmen.
4. macOS schmuggelt Metadaten in das Archiv
Symptom: Im ausgelieferten Verzeichnis liegen Dateien wie ._index.html und .DS_Store.
Ursache: tar übernimmt auf macOS erweiterte Attribute und legt dafür AppleDouble-Dateien an. Die landen unverändert auf dem Server.
Prüfung: Der Befehl aus Schritt 4, er muss 0 liefern.
Lösung: COPYFILE_DISABLE=1 und --no-xattrs beim Packen, plus ein aufräumendes find auf dem Server als zweite Verteidigungslinie. Wer vom Mac auf Linux deployt, sollte beides setzen.
5. Die geänderte Konfigurationsdatei greift nicht
Symptom: Sie ändern die nginx.conf auf dem Server, starten den Container neu, und er läuft weiter mit der alten Konfiguration.
Ursache: Dieselbe Inode-Falle wie bei Stolperstein 2, nur für eine einzelne Datei. Ein Bind-Mount auf eine Datei hängt am Inode. Werkzeuge wie sed -i schreiben nicht in die Datei, sondern legen eine neue an und benennen sie um. Der Container sieht die neue nie.
Prüfung:
docker compose exec web head -20 /etc/nginx/conf.d/default.confLösung: Solche Dateien im Editor bearbeiten statt mit sed -i, und danach den Container neu erzeugen:
docker compose up -d --force-recreate web--force-recreate zwingt Compose dazu, auch dann neu anzulegen, wenn sich Image und Konfiguration scheinbar nicht geändert haben, siehe Docker-Doku zu docker compose up. Wer das Verzeichnis statt der Einzeldatei mountet, hat das Problem grundsätzlich nicht.
6. Das Skript läuft auf dem Mac, aber nicht auf Linux
Symptom: Dasselbe Skript, das auf dem Arbeitsrechner läuft, bricht auf einem Linux-Server sofort ab mit mktemp: too few X's in template.
Ursache: mktemp -d -t name ist BSD-Syntax. GNU coreutils verlangt mindestens sechs X im Template. Der Abbruch passiert in der ersten Zeile, lange bevor irgendetwas Interessantes geschieht.
Prüfung: Vor dem Umzug eines Skripts auf einen anderen Rechner:
bash -n deploy.sh && mktemp -d -t testnameLösung: Die explizite Form nutzen, sie gilt auf beiden Systemen:
mktemp -d "${TMPDIR:-/tmp}/name.XXXXXX"Das ist ein guter Anlass, das ganze Skript mit shellcheck durchzusehen. Portabilitätsfallen dieser Art gibt es mehr als eine.
7. Der Build im Container scheitert an den Rechten des Cache-Volumes
Dieser Stolperstein betrifft nur den Ausbau auf einen zentralen Build-Host, dafür trifft er dort zuverlässig.
Symptom: npm ci bricht im Container ab mit EACCES beim Anlegen des Cache-Verzeichnisses.
Ursache: Ein Named Volume übernimmt beim ersten Mounten die Rechte des Pfades aus dem Image. Existiert der Pfad im Image nicht, legt Docker ihn als root an. Ein Container, der bewusst als unprivilegierter Benutzer läuft, kommt dann nicht hinein.
Prüfung:
docker run --rm -v npm-cache:/cache alpine ls -ld /cacheLösung: Das Verzeichnis im Dockerfile selbst anlegen und die Rechte setzen, bevor das Volume ins Spiel kommt:
RUN mkdir -p /home/node/.npm && chown -R node:node /home/node/.npm
ENV NPM_CONFIG_CACHE=/home/node/.npmEin bereits falsch angelegtes Volume muss einmal weg, sonst bleiben die alten Rechte bestehen:
docker volume rm npm-cacheDie Ausbaustufe: ein zentraler Build-Host
Sobald an zwei Rechnern gearbeitet wird, ist die Build-Umgebung die eigentliche Fehlerquelle. Unterschiedliche Node-Versionen, unterschiedlich altes node_modules, unterschiedliche Betriebssysteme. Der Ausweg ist ein Container, der die Toolchain festlegt, und ein Host, der ihn ausführt.
Der Zuschnitt, der sich bei mir bewährt hat, trennt strikt nach Vertrauensbedarf:
- Der Host hält die Zugangsdaten und macht
gitund den Deploy. - Der Container baut, sieht nur das Quellverzeichnis und bekommt kein einziges Geheimnis. Kein SSH-Schlüssel, kein Zugang zum Zielserver.
Der Vorteil ist nicht nur Reproduzierbarkeit. Ein Build-Schritt führt fremden Code aus, nämlich Ihre Abhängigkeiten und deren Installationsskripte. Ein Container ohne Zugangsdaten kann selbst dann nichts veröffentlichen, wenn in einem Paket etwas Bösartiges steckt.
Wer diesen Schritt geht, baut außerdem aus dem Git-Stand statt aus dem Arbeitsbaum. Damit ist Stolperstein 1 nicht mehr entschärft, sondern strukturell ausgeschlossen.
Grenzen
Diese Pipeline hat kein Rollback. Der alte Stand wird überschrieben, und was weg ist, ist weg. Wenn Ihnen das zu knapp ist, halten Sie den vorherigen dist/-Stand auf dem Server vor und schalten Sie über einen Symlink um, statt Dateien zu ersetzen.
Sie hat auch keine Vorschau je Branch und keine Testintegration. Beides ist der Punkt, an dem eine richtige CI/CD-Strecke das bessere Werkzeug ist. Für den souveränen Betrieb weniger Seiten auf eigenem Server ist der Ein-Befehl-Weg schlank, nachvollziehbar und ohne Fremd-Abhängigkeit.
Und sie setzt Disziplin voraus. Ein Deploy-Skript ohne die Prüfung aus Stolperstein 1 ist eine Veröffentlichungsmaschine für unfertige Arbeit. Das ist keine theoretische Sorge, sondern der Fehler, der von allen sieben am teuersten wird.
Sie wollen Ihre Seiten auf eigenem Server statt in einer fremden Cloud betreiben? Ich richte eine schlanke, nachvollziehbare Deploy-Strecke ein, die Ihnen gehört. Wie ich Infrastruktur souverän aufsetze, lesen Sie unter Infrastruktur & Cloud, das Erstgespräch ist unverbindlich.

Alex Jabi
Ich betreue IT, Informationssicherheit und Datenschutz für KMU und Praxen an der Bergstraße und im Odenwald, persönlich, dokumentiert und ohne Cloud-Zwang.
Mehr über mich


