Deployn

Astro.js & Docker: Dein Weg zur Live-Webseite (Statisch & Dynamisch auf dem VPS)

Astro.js mit Docker auf deinem VPS: Statisch (Nginx) oder dynamisch (Node.js). Komplettes Tutorial mit aktuellen Dockerfiles, Docker Compose und Deployment-Workflow.

Veröffentlicht am Aktualisiert am
Astro.js & Docker: Dein Weg zur Live-Webseite (Statisch & Dynamisch auf dem VPS) hero image
Inhalt
Getestet mit
Astro 7.4@astrojs/node 11.1Node.js 26 (node:lts)Nginx 1.31Caddy 2.11Docker 29.9Ubuntu 24.04
Zuletzt geprüft am

Du möchtest eine moderne, schnelle Webseite mit Astro.js bauen und sie auf deinem eigenen Server bereitstellen? Docker ist dafür das passende Werkzeug, inzwischen setzen 92% der IT-Branche auf Container (Docker State of App Dev Report, 2025). In diesem Beitrag führe ich durch den gesamten Ablauf: von der Entwicklungsumgebung bis zum Live-Deployment der Astro-Anwendung mit Docker und Docker Compose auf einem VPS.

Kernpunkte auf einen Blick

  • Astro wuchs von 29 Domains (2021) auf über 38.800 (2025), das 1.340-fache (TechnologyChecker.io, 2025).
  • Statische Builds per Nginx: schlank (~94 MB Image), extrem schnell.
  • Dynamische Builds per Node.js-Adapter: flexibel, für SSR-Routen nötig.
  • Multi-Stage Dockerfiles halten die Produktions-Images klein und sicher.
  • Docker Compose orchestriert den Start mit einem Befehl.

Wie bereitest du deine Entwicklungsumgebung vor?

Drei Werkzeuge bilden das Fundament für die Astro- und Docker-Entwicklung.

  1. Text-Editor: Visual Studio Code (VS Code) ist kostenlos und bietet eine riesige Erweiterungsbibliothek. Die Astro-Erweiterung für VS Code bringt Syntax-Highlighting und IntelliSense.

  2. Node.js: Astro basiert auf Node.js. Statt einer direkten Installation empfehle ich einen Versionsmanager, um flexibel zwischen Versionen zu wechseln.

    • Windows: nvm-windows vermeidet Rechteprobleme.
    • Linux/macOS: Das originale nvm (Node Version Manager) lässt sich per curl- oder wget-Skript installieren.
    • Astro 7 verlangt Node.js ab Version 22.23.4. Die LTS-Version erfüllt das.
    Terminalbash
    nvm install lts # Neueste LTS-Version installieren
    nvm use lts     # LTS-Version aktivieren
  3. Git: Unverzichtbar für Versionskontrolle und den einfachen Transfer zum Server. Git herunterladen.

    Terminalbash
    # Während der Installation unter Windows: VS Code als Standard-Editor wählen
    # Default Branch Name auf "main" setzen

Öffne dein Terminal (in VS Code: Strg+Shift+Ö) und prüfe die Installationen:

Terminalbash
node -v
npm -v
git --version
nvm version

Wie erstellst du dein erstes Astro-Projekt?

Wechsle in deinen Projektordner und starte die Astro-CLI. Das Framework generiert grundlegende Dateien und installiert alle Abhängigkeiten automatisch.

Terminalbash
# Projekt mit minimalem Template erstellen
npm create astro@latest -- --template minimal mein-astro-projekt

# In den Projektordner wechseln
cd mein-astro-projekt

# Abhängigkeiten installieren (oft schon während der Erstellung erledigt)
npm install
# oder pnpm install

# Entwicklungs-Server starten
npm run dev
# oder pnpm dev

Standardmäßig findet man nun unter http://localhost:4321 die Astro-Standardseite.

Änderungen in src/pages/index.astro werden per Hot Module Replacement sofort sichtbar.

Astro verstehen: Seiten, Layouts und Komponenten

Astro-Projekte folgen einer klaren Struktur. Wer diese einmal verstanden hat, kann Projekte jeder Größe aufbauen.

  • src/pages/: Jede .astro-Datei hier wird zu einer Route. index.astro wird zu /, about.astro zu /about.
  • src/layouts/: Wiederverwendbare Seitenstrukturen. Ein typisches Layout (BaseLayout.astro) enthält das HTML-Gerüst mit einem <slot />, den jede nutzende Seite füllt.
  • src/components/: Wiederverwendbare UI-Elemente wie Header, Footer oder Karten. Importierbar in Seiten und Layouts.
  • Styling: Globale CSS-Dateien importierst du in Layouts. Innerhalb einer .astro-Datei sind <style>-Tags standardmäßig scoped, gelten also nur für die jeweilige Komponente.
astro
---
// Beispiel: src/layouts/BaseLayout.astro
import Header from "../components/Header.astro";
import Footer from "../components/Footer.astro";
import "../styles/global.css";

interface Props {
	title: string;
	description?: string;
}

const { title, description = "Meine Astro Seite" } = Astro.props;
---

<!doctype html>
<html lang="de">
	<head>
		<meta charset="UTF-8" />
		<meta name="viewport" content="width=device-width, initial-scale=1.0" />
		<meta name="description" content={description} />
		<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
		<title>{title} | Astro Demo</title>
	</head>
	<body>
		<Header />
		<main class="container">
			<slot />
		</main>
		<Footer />
	</body>
</html>

<style>
	.container {
		max-width: 1100px;
		margin-left: auto;
		margin-right: auto;
		padding-left: 1rem;
		padding-right: 1rem;
		flex-grow: 1;
	}
	body {
		display: flex;
		flex-direction: column;
		min-height: 100vh;
	}
</style>

Statisch oder dynamisch: Welcher Build passt zu dir?

Nginx liefert statische Dateien unter Last etwa 4.874-mal schneller aus als ein Node.js-Server, mit einer medianen Antwortzeit von 0,05 ms gegenüber 253 ms (Benchmark, Adam Jones, 2024). Für reine Auslieferung ist der Nginx-Weg also klar überlegen. Doch nicht jedes Projekt kommt mit statischen Dateien aus.

Ich wollte die Zahlen nicht nur zitieren und habe selbst gemessen. Auf einem Hetzner-Server (2 vCPU, 4 GB RAM) lief jede Variante in einem Container, und ich habe mit ab -n 20000 -c 50 -k je 20.000 Anfragen abgeschickt. Das Messwerkzeug lief auf derselben Maschine und nimmt dem Server etwas Rechenzeit, die absoluten Werte sind also niedriger als bei einer Messung von außen. Das Verhältnis bleibt aussagekräftig:

Variante Anfragen pro Sekunde Zeit pro Anfrage RAM nach dem Test
Nginx, statische Seite 15.150 bis 15.880 (2 Läufe) 3,1 bis 3,3 ms 8 MiB
Node.js, vorgerenderte Seite 5.871 8,5 ms -
Node.js, SSR-Route 1.838 27,2 ms 153 MiB

Selbst eine vorgerenderte Seite liefert Nginx rund 2,7-mal so schnell aus wie der Node-Server, und eine SSR-Route braucht etwa das 8,6-Fache an Zeit. Dafür kostet SSR nur Rechenzeit, wenn die Seite wirklich dynamisch sein muss.

Der statische Build (Standard)

Das ist der Normalfall bei Astro. npm run build generiert reines HTML, CSS und JavaScript im dist/ Ordner. Kein Node.js-Server zur Laufzeit nötig.

Terminalbash
npm run build

Den dist/ Ordner kannst du auf jedem statischen Webhost bereitstellen.

Dynamische Routen mit dem Node-Adapter

Braucht eine Seite dynamische Daten, die sich bei jedem Aufruf ändern (z.B. Nutzerinfos aus Request-Headern)? Dann kommt Server-Side Rendering (SSR) ins Spiel.

  1. Adapter installieren:

    Terminalbash
    npx astro add node

    Das passt deine astro.config.mjs an und installiert die nötigen Pakete.

  2. Prerendering deaktivieren: Für die dynamische Seite explizit ausschalten:

    astro
    ---
    // src/pages/request-info.astro
    export const prerender = false;
    
    const userAgent = Astro.request.headers.get("user-agent") || "Unbekannt";
    
    import BaseLayout from "../layouts/BaseLayout.astro";
    ---
    
    <BaseLayout title="Request Info">
    	<h1>Deine Request Info</h1>
    	<p>Dein User-Agent: {userAgent}</p>
    </BaseLayout>
  3. Build-Ergebnis: Der dist/ Ordner enthält jetzt neben statischen Assets (client/) auch Server-Code (server/entry.mjs), der zur Laufzeit mit Node.js ausgeführt werden muss.

Docker als Fundament deines Deployments

Docker verpackt deine Anwendung samt aller Abhängigkeiten in einen isolierten Container. Das Resultat: deine App läuft in derselben Umgebung, lokal wie auf dem Server. Docker verzeichnet in der IT-Branche eine Adoptionsrate von 92% im Jahr 2025, ein Plus von 12 Prozentpunkten gegenüber dem Vorjahr (Docker State of App Dev Report, 2025). Auch in der Self-Hosting Community wird es aus mehreren Gründen sehr gerne verwendet. Containerisierung ist kein Nischenthema, sondern Standard.

Die .dockerignore: Was nicht ins Image gehört

Ähnlich wie .gitignore teilt diese Datei Docker mit, welche Dateien nicht kopiert werden sollen. Das beschleunigt den Build und hält das Image klein.

# .dockerignore
.DS_Store
node_modules
npm-debug.log
dist
.astro
.env*
*.env
.git
.vscode
Dockerfile*
docker-compose*
compose*
README.md

Wie containerisierst du eine statische Astro-Seite?

Für rein statische Seiten nutze ich einen zweistufigen Docker Build. Die Build-Stage erzeugt die Dateien mit Node.js, die Runtime-Stage liefert sie per Nginx aus. Das entspricht dem offiziellen Astro-Docker-Rezept.

Dockerfile (statische Seite):

Dockerfiledockerfile
# ---- Build Stage ----
FROM node:lts-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# ---- Runtime Stage ----
FROM nginx:alpine AS runtime
COPY ./nginx/nginx.conf /etc/nginx/nginx.conf
COPY --from=build /app/dist /usr/share/nginx/html
EXPOSE 80
  • node:lts-alpine ist die schlanke Variante des Node-Images. Für den Build der Seite reicht sie, und der Download ist deutlich kleiner.
  • npm ci installiert exakt die Versionen aus der package-lock.json und bricht ab, wenn Lockfile und package.json nicht zusammenpassen. Ein Build liefert so auf jedem Rechner dasselbe Ergebnis. Nutzt du pnpm, ersetze die Zeilen durch corepack enable und pnpm install --frozen-lockfile.
  • Nur die Runtime-Stage landet im fertigen Image. Node.js und node_modules bleiben in der Build-Stage zurück.

src/pages/404.astro:

Damit Nginx unbekannte Adressen sauber beantworten kann, braucht die Seite eine eigene 404-Seite. Astro baut daraus die Datei 404.html.

src/pages/404.astroastro
---
import BaseLayout from "../layouts/BaseLayout.astro";
---

<BaseLayout title="Seite nicht gefunden">
	<h1>404: Seite nicht gefunden</h1>
	<p><a href="/">Zurück zur Startseite</a></p>
</BaseLayout>

nginx/nginx.conf:

Lege die Konfigurationsdatei im Ordner nginx/ im Projektroot ab. Sie aktiviert Gzip, setzt Cache-Header für die Dateien mit Hash im Namen und sperrt versteckte Dateien.

nginx/nginx.confnginx
worker_processes auto;

events {
    worker_connections 1024;
}

http {
    include /etc/nginx/mime.types;
    default_type application/octet-stream;

    server {
        listen 80;
        server_name _;
        root /usr/share/nginx/html;
        index index.html;

        gzip on;
        gzip_vary on;
        gzip_proxied any;
        gzip_comp_level 6;
        gzip_min_length 1000;
        gzip_types text/plain text/css text/xml application/json
                   application/javascript application/xml+rss
                   application/atom+xml image/svg+xml;

        # Astro hängt Hashes an Dateien in /_astro/, die dürfen ewig gecacht werden
        location /_astro/ {
            add_header Cache-Control "public, max-age=31536000, immutable";
            access_log off;
        }

        location ~ /\. {
            deny all;
        }

        location / {
            try_files $uri $uri/ =404;
        }

        error_page 404 /404.html;
    }
}

Drei Entscheidungen in dieser Datei:

  • try_files $uri $uri/ =404: Früher stand hier ein Fallback auf die index.html. Das passt zu Single-Page-Apps, aber nicht zu einer statischen Astro-Seite: Jede falsche Adresse liefert dann die Startseite mit Status 200 aus, und kaputte Links fallen nicht auf. Jetzt antwortet Nginx mit einem echten 404 und zeigt deine 404.html.
  • Cache nur für /_astro/: Astro hängt an diese Dateien einen Hash an. Ändert sich der Inhalt, ändert sich der Name, deshalb darf der Browser sie ein Jahr lang behalten. Alles andere (HTML, Favicon) wird bei jedem Besuch geprüft.
  • worker_processes auto: Im Container sieht Nginx alle CPU-Kerne des Hosts. auto startet pro Kern einen Worker, das macht unter Last einen spürbaren Unterschied.

Das fertige Image ist auf meinem Testserver 94 MB groß.

Wie containerisierst du eine dynamische Astro-Seite?

Mit dem Node-Adapter brauchst du Nginx nicht. Die Dockerfile folgt dem Multi-Stage-Pattern aus den Astro-Docs, das Produktions-Abhängigkeiten von Build-Abhängigkeiten trennt. Das beschleunigt spätere Builds, wenn sich nur der Quellcode geändert hat.

Dockerfile (dynamische Seite mit Node.js):

Dockerfiledockerfile
# ---- Base Stage ----
FROM node:lts-alpine AS base
WORKDIR /app
COPY package.json package-lock.json ./

# ---- Production Dependencies ----
FROM base AS prod-deps
RUN npm ci --omit=dev

# ---- Build Dependencies + Build ----
FROM base AS build-deps
RUN npm ci

FROM build-deps AS build
COPY . .
RUN npm run build

# ---- Runtime Stage ----
FROM base AS runtime
COPY --from=prod-deps /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
ENV HOST=0.0.0.0
ENV PORT=4321
EXPOSE 4321
USER node
CMD ["node", "./dist/server/entry.mjs"]
  • Cache: Ändert sich nur dein Quellcode, nicht aber package.json, überspringt Docker die beiden npm ci-Schritte und geht direkt zum Build. Bei großen Projekten spart das Minuten.
  • HOST=0.0.0.0: Ohne diese Zeile lauscht der Server nur auf localhost im Container, und von außen kommt nichts an.
  • USER node: Der Server läuft nicht als root. Das Image node:lts-alpine bringt dafür den Benutzer node schon mit. Im Container zeigt id danach uid=1000(node).
  • Größe: Mit node:lts-alpine ist das Image 443 MB groß. Das gleiche Dockerfile mit node:lts (Debian) kommt auf 1,97 GB. Einen großen Teil der 443 MB macht node_modules aus, weil Astro selbst zur Laufzeit gebraucht wird.

Docker Compose: Container orchestrieren

docker compose vereinfacht das Starten und Konfigurieren enorm. Auch für einen einzelnen Container lohnt es sich.

compose.yml:

compose.ymlyaml
services:
    astro-app:
        build:
            context: .
            dockerfile: Dockerfile
        container_name: mein-astro-container
        ports:
            # Statische Seite (Nginx auf Port 80 im Container)
            - "80:80"
            # Dynamische Seite (Node.js auf Port 4321 im Container)
            # - "80:4321"
        restart: unless-stopped
        healthcheck:
            # Bei der dynamischen Seite: http://127.0.0.1:4321/
            test: ["CMD", "wget", "-q", "--spider", "http://127.0.0.1/"]
            interval: 30s
            timeout: 5s
            retries: 3
  • Dateiname: compose.yml, compose.yaml oder docker-compose.yml funktionieren alle, Docker Compose v2 erkennt jede Variante.
  • ports: Wähle das passende Mapping: 80:80 für Nginx, 80:4321 für den Node-Adapter. Die linke Zahl ist der Port auf dem Server, den du im Browser aufrufst.
  • healthcheck: Docker fragt alle 30 Sekunden die Seite ab und zeigt in docker compose ps den Zustand healthy oder unhealthy. wget bringt alpine bereits mit, du musst nichts nachinstallieren.
  • image:-Zeile: Ich lasse sie weg. Compose benennt das Image dann nach dem Projekt, und du hast kein latest-Tag im Spiel.

Wie deployest du deine Astro-App auf den VPS?

Jetzt bringen wir alles zusammen. Ich habe jeden Schritt auf einem Server mit Ubuntu 24.04 durchgespielt, einmal mit der statischen und einmal mit der dynamischen Variante. Du brauchst:

  • einen VPS mit Ubuntu 24.04 oder einem anderen Docker-kompatiblen System. Wenn du noch keinen hast, hilft dir mein VPS-Leitfaden für Anfänger bei Auswahl und Absicherung. Mit dem Cloud-Init-Konfigurator kannst du einen Hetzner-Server samt Docker und SSH-Key direkt beim Anlegen einrichten lassen und dir die Installation aus Schritt 1 sparen.
  • ein Git-Repository mit deinem Projekt, zum Beispiel auf GitHub.
  • optional eine Domain, wenn du die Seite per HTTPS ausliefern willst.

Der Ablauf im Überblick:

  1. Docker auf dem Server installieren
  2. Code auf den Server holen
  3. Container bauen und starten
  4. Status prüfen und Seite aufrufen
  5. Firewall bedenken
  6. Domain und HTTPS mit Caddy
  7. Updates einspielen

Wenn dir die Befehle im Terminal noch fremd sind, hilft dir mein Linux-Terminal-Guide.

1. Docker auf dem Server installieren

Verbinde dich per SSH mit deinem Server:

Terminalbash
ssh root@{{server_ip}}

Installiere dann Docker inklusive Compose-Plugin. Die Befehle gelten für Ubuntu:

Terminalbash
sudo apt-get update
sudo apt-get install -y ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc

echo \
 "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
 $(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}") stable" | \
 sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin git

Prüfe danach, ob alles da ist:

Terminalbash
docker --version
docker compose version
Docker version 29.9.3, build 5457199
Docker Compose version v5.7.0

Die Compose-Unterstützung steckt als Plugin schon drin, du rufst sie mit docker compose (ohne Bindestrich) auf. Eine ausführlichere Einrichtung eines Servers mit Docker findest du in meinem Beitrag zum Ubuntu Homeserver.

2. Code auf den Server holen

Klone dein Repository, das du vorher gepusht hast:

Terminalbash
git clone DEINE_REPOSITORY_URL mein-astro-projekt
cd mein-astro-projekt

Ist das Repository privat, brauchst du auf dem Server einen Zugang: entweder einen Deploy Key oder einen Access Token für die HTTPS-URL. Falls du Git noch nicht kennst, erklärt mein Git-Tutorial die Grundlagen.

Du kannst auch ohne Git arbeiten und den Projektordner von deinem Rechner kopieren. rsync überträgt dabei nur, was sich geändert hat:

Terminalbash
rsync -az --exclude node_modules --exclude dist --exclude .astro --exclude .git \
  ./ root@{{server_ip}}:/root/mein-astro-projekt/

3. Container bauen und starten

Im Projektordner auf dem Server:

Terminalbash
sudo docker compose up --build -d

docker compose up startet die Services aus der compose.yml. --build erzwingt, dass das Image neu gebaut wird, falls sich Code oder Dockerfile geändert haben. -d startet alles im Hintergrund. Der erste Build dauert, weil Docker die Basis-Images herunterladen muss. Bei mir lagen auf dem Server zwischen dem Befehl und dem laufenden Container 13 Sekunden.

4. Status prüfen und Seite aufrufen

Direkt nach dem Start zeigt docker compose ps den Zustand health: starting. Nach dem ersten erfolgreichen Healthcheck, also nach rund 30 Sekunden, steht dort healthy:

Terminalbash
sudo docker compose ps
NAME                   IMAGE                          COMMAND                  SERVICE     CREATED          STATUS                    PORTS
mein-astro-container   mein-astro-projekt-astro-app   "/docker-entrypoint.…"   astro-app   35 seconds ago   Up 35 seconds (healthy)   0.0.0.0:80->80/tcp, [::]:80->80/tcp

Ob der Server antwortet, prüfst du direkt auf dem Server mit curl:

Terminalbash
curl -I http://localhost
HTTP/1.1 200 OK
Server: nginx/1.31.8
Content-Type: text/html

Öffne nun http://SERVER-IP in deinem Browser. Deine Astro-Webseite ist live.

Die statische Astro-Seite im Browser, ausgeliefert von Nginx im Container

Läuft der Container nicht, siehst du die Ursache in den Logs:

Terminalbash
sudo docker compose logs -f astro-app

5. Firewall: Docker umgeht ufw

Wer auf dem Server die Firewall ufw benutzt, erlebt bei Docker eine Überraschung. Ich habe ufw aktiviert und nur Port 22 freigegeben. Der Container war trotzdem von außen erreichbar. Docker trägt veröffentlichte Ports direkt in die Firewall-Regeln des Kernels ein, und ufw sieht diese Regeln nicht.

Zwei Wege helfen:

  • Veröffentliche Ports, die nicht öffentlich sein sollen, nur lokal: "127.0.0.1:8080:80" statt "8080:80".
  • Setze zusätzlich die Cloud-Firewall deines Anbieters ein. Sie sitzt vor dem Server, und Docker kann sie nicht umgehen. Mehr zur Absicherung steht im VPS-Leitfaden.

6. Domain und HTTPS mit Caddy

Eine Seite unter http://SERVER-IP ist für einen Test in Ordnung. Für echte Besucher brauchst du eine Domain und HTTPS. Ich nutze dafür Caddy als Reverse Proxy: Er holt das Zertifikat bei Let’s Encrypt selbst und erneuert es automatisch.

  1. Lege bei deinem Domain-Anbieter einen A-Record für astro.domain.de an, der auf SERVER-IP zeigt. Warte, bis die Namensauflösung funktioniert.
  2. Erweitere die compose.yml. Die Astro-App veröffentlicht keinen Port mehr, nur Caddy ist von außen erreichbar:
compose.ymlyaml
services:
    astro-app:
        build:
            context: .
            dockerfile: Dockerfile
        container_name: mein-astro-container
        ports:
            - "80:80"
        restart: unless-stopped
        healthcheck:
            test: ["CMD", "wget", "-q", "--spider", "http://127.0.0.1/"]
            interval: 30s
            timeout: 5s
            retries: 3

    caddy:
        image: caddy:2
        container_name: caddy
        restart: unless-stopped
        ports:
            - "80:80"
            - "443:443"
            - "443:443/udp"
        volumes:
            - ./Caddyfile:/etc/caddy/Caddyfile:ro
            - caddy_data:/data
            - caddy_config:/config
        depends_on:
            - astro-app

volumes:
    caddy_data:
    caddy_config:
  1. Lege die Caddyfile neben die compose.yml. Bei der statischen Seite zeigt sie auf Port 80 des Containers, bei der dynamischen auf 4321:
Caddyfilecaddy
{{domain}} {
    reverse_proxy astro-app:80
}

Dabei ist astro-app der Service-Name aus der compose.yml. Compose-Services im selben Projekt erreichen sich über diesen Namen. Wenn du dein Setup lieber per Formular zusammenklickst, hilft dir der Caddyfile-Generator. Wer Traefik statt Caddy einsetzen will, findet das in meiner Anleitung zu Traefik als Reverse Proxy.

  1. Starte alles neu:
Terminalbash
sudo docker compose up -d

Caddy holt das Zertifikat beim ersten Aufruf. Bei mir stand es nach rund fünf Sekunden bereit. Im Log erscheint die Zeile certificate obtained successfully. Prüfe beide Adressen:

Terminalbash
curl -I https://{{domain}}
curl -I http://{{domain}}
HTTP/2 200
...
HTTP/1.1 308 Permanent Redirect
Location: https://{{domain}}/

HTTP leitet Caddy also von selbst auf HTTPS um. So sieht die dynamische Seite aus, wenn du /request-info aufrufst:

Die dynamische Seite zeigt den User-Agent des Browsers, ausgeliefert vom Node-Server hinter Caddy

7. Updates einspielen

Hast du lokal Änderungen gemacht, läuft das Update immer gleich ab:

  1. Code committen und pushen
  2. Auf dem Server den neuen Stand holen und neu bauen:
Terminalbash
cd mein-astro-projekt
git pull
sudo docker compose up --build -d
  1. Alte Image-Schichten entfernen, damit der Server nicht vollläuft:
Terminalbash
sudo docker image prune -f

Docker baut das neue Image und ersetzt den Container. Bei der dynamischen Seite lassen sich unveränderte npm ci-Schritte aus dem Cache wiederverwenden, das Update geht dann schneller.

Ist ein Update schiefgegangen, wechsle auf dem Server zum letzten funktionierenden Commit und baue neu: git log --oneline zeigt dir die Stände, git checkout COMMIT_ID setzt den Code zurück, danach wieder sudo docker compose up --build -d. Mit git checkout main kehrst du später zum aktuellen Stand zurück.

Wenn etwas nicht klappt

  • Bind for :::80 failed: port is already allocated: Ein anderer Container oder Dienst belegt schon Port 80. sudo docker ps zeigt dir, welcher. Stoppe ihn oder wähle für die Astro-App einen anderen Host-Port, zum Beispiel "8080:80".
  • 502 von Caddy: Der Proxy erreicht den Container nicht. Prüfe, ob der Port in der Caddyfile zum Container passt (80 bei Nginx, 4321 bei Node.js) und ob du Caddy nach einer Änderung neu gestartet hast.
  • [NoAdapterInstalled] beim Build: Eine Seite setzt prerender = false, aber der Node-Adapter fehlt. Installiere ihn mit npx astro add node und committe die geänderte astro.config.mjs samt package.json und Lockfile.
  • Container startet nicht oder startet ständig neu: Die Ursache steht fast immer in sudo docker compose logs astro-app.

Häufige Fragen

Kann ich Astro auch ohne Docker deployen?

Ja. Astro generiert statische Dateien, die auf jedem Webserver hostbar sind. Mit dem Node.js-Adapter erzeugt Astro außerdem einen Server, der auf jedem Rechner mit Node.js läuft. Docker macht beides reproduzierbar und lässt sich mit einem Befehl starten.

Wann brauche ich Server-Side Rendering?

SSR ist nötig, wenn Seiten bei jedem Aufruf aktuelle Daten brauchen, etwa nutzerspezifische Inhalte oder Echtzeit-Abfragen. Für Blogs, Portfolios und Dokumentationsseiten reicht statisches Rendering völlig aus. Es ist schneller und braucht keinen laufenden Serverprozess.

Wie viel Speicher und Rechenleistung braucht eine Astro-Seite im Container?

In meinem Test auf einem Server mit 2 vCPU und 4 GB RAM belegte der Nginx-Container nach 20.000 Anfragen 8 MiB. Der Node.js-Container kam im Leerlauf auf 70 MiB und nach dem Test auf 153 MiB. Das Nginx-Image ist 94 MB groß, das Node.js-Image mit node:lts-alpine 443 MB.

Warum ist mein Node.js-Image so groß?

Das Basis-Image node:lts (Debian) allein wiegt rund 1,8 GB. Mit node:lts-alpine schrumpft das fertige Image von 1,97 GB auf 443 MB. Bei einer rein statischen Seite umgehst du das Problem, weil nur Nginx im fertigen Image landet.

Wie aktualisiere ich meine Webseite auf dem Server?

Lokal änderst du den Code, testest und pushst per Git. Auf dem Server führst du git pull und danach docker compose up --build -d aus. Das baut das Image neu und ersetzt den Container. Mit docker image prune -f räumst du alte Images weg.

Wie bekomme ich HTTPS für meine Astro-Seite?

Setze einen Reverse Proxy wie Caddy vor den Container. Du legst einen A-Record für deine Domain an, ergänzt Caddy in der compose.yml und trägst die Domain in die Caddyfile ein. Das Zertifikat holt Caddy bei Let's Encrypt selbst. Die genaue Konfiguration steht im Abschnitt zu Domain und HTTPS.

Warum ist mein Container trotz ufw von außen erreichbar?

Docker trägt veröffentlichte Ports direkt in die Firewall-Regeln des Kernels ein, und ufw sieht diese Regeln nicht. Veröffentliche Ports, die privat bleiben sollen, deshalb mit 127.0.0.1 davor (zum Beispiel 127.0.0.1:8080:80) oder sperre sie in der Cloud-Firewall deines Anbieters.

Kann ich mehrere Astro-Projekte auf einem VPS hosten?

Ja. Jedes Projekt bekommt seinen eigenen Container. Davor setzt du einen Reverse Proxy wie Caddy oder Traefik, der anhand der Domain auf den richtigen Container verteilt. Alternativ veröffentlichst du jedes Projekt auf einem eigenen Port.

Nginx oder Apache für statische Astro-Seiten?

Ich nutze Nginx: Es steht im offiziellen Astro-Docker-Rezept, das Image ist klein und die Konfiguration kurz. Apache liefert statische Dateien genauso aus.

Fazit

Astro.js und Docker ergänzen sich gut. Astro liefert die Performance und Entwicklerfreundlichkeit, Docker sorgt für konsistente, portable Deployments. Mit den Dockerfiles, der compose.yml und der Caddy-Konfiguration aus diesem Beitrag kommt deine Seite in wenigen Minuten auf einen eigenen Server, per HTTPS unter deiner Domain.

Wenn du mehr Dienste auf demselben Server betreiben willst, lohnt sich ein gemeinsamer Reverse Proxy: Wie das mit Traefik funktioniert, zeigt mein Beitrag Server mit Traefik als Reverse Proxy einrichten. Bei Fragen oder Feedback schreib gern einen Kommentar.

Teilen
XRedditE-Mail
Zurück zu allen Beiträgen

Änderungsprotokoll

  1. Auf Astro 7 getestet. Deployment-Abschnitt neu geschrieben, Dockerfiles auf node:lts-alpine und npm ci umgestellt, Nginx-Konfiguration aktualisiert (echtes 404, Cache-Header), Messwerte und Screenshots ergänzt.

  2. Dockerfiles aktualisiert (node:lts, optimiertes Multi-Stage-Pattern), Text ergänzt, FAQ-Sektion hinzugefügt.

Verwandte Artikel

Kommentare