Dealownia
Menu
sprawdzono – jak sprawdzamy oferty

Strona Astro na własnym VPS: nginx i wdrożenie bez przestoju

Build Astro, katalogi wydań z atomową podmianą dowiązania, nginx z kompresją i nagłówkami cache, cofnięcie wdrożenia jedną komendą. Ubuntu 24.04 i Debian.

Hosting i stronyzaktualizowano 7 min czytania

Redakcja serwisu Dealownia · ceny i fakty ze stron dostawców, jak sprawdzamy

Statyczna strona Astro na własnym VPS, wdrażana jednym skryptem: każde wdrożenie trafia do osobnego katalogu, a strona przełącza się na nową wersję w jednej operacji, bez okna, w którym część plików jest stara, a część nowa. Cofnięcie do poprzedniej wersji to jedna komenda. nginx wyśle pliki skompresowane, a przeglądarki będą trzymać w pamięci podręcznej to, co się nie zmienia.

Czego potrzebujesz

  • VPS z Ubuntu 24.04 LTS albo Debianem 12 lub 13, z nginx i certyfikatem Let’s Encrypt – przygotowany jak w poradniku od zera. Strona statyczna nie potrzebuje dużego serwera: wystarczy najmniejszy plan z 1 vCPU i 1 GB RAM.
  • Projekt Astro bez renderowania na serwerze (domyślny tryb statyczny). Astro wymaga Node.js w wersji LTS – dokładną minimalną wersję podaje dokumentacja instalacji Astro.
  • Na swoim komputerze: bash, ssh i rsync (na Windowsie najprościej w WSL). rsync musi być też na serwerze: sudo apt install -y rsync.
  • Konto deploy na serwerze, na które logujesz się kluczem SSH.

Plany z parametrami porównasz w rankingu VPS.

Krok 1. Build lokalnie

W astro.config.mjs ustaw adres strony – Astro używa go do adresów kanonicznych i mapy strony:

javascript
import { defineConfig } from 'astro/config';

export default defineConfig({
  site: 'https://example.pl',
  trailingSlash: 'always',
});

Domyślnie build.format to 'directory' (każda strona to katalog z index.html), a wynik trafia do dist/. Zbuduj i sprawdź, czy jest strona główna i strona błędu:

bash
npm ci
npm run build
ls dist/index.html dist/404.html

404.html powstanie, jeśli masz plik src/pages/404.astro. Bez niego nginx pokaże swoją domyślną stronę błędu.

Budować możesz na swoim komputerze (jak w tym poradniku) albo na serwerze po każdym pushu do repozytorium. Drugi sposób wymaga Node.js na VPS-ie i trochę pamięci na czas budowania, ale nie zależy od tego, z którego komputera wdrażasz. Jeśli budujesz na serwerze, publikuj wydanie dopiero po udanym buildzie i testach – nieudany build zostawi wtedy w sieci poprzednią wersję. Zasada katalogów wydań z kolejnych kroków działa tak samo w obu wariantach.

Krok 2. Katalogi wydań na serwerze

Każde wdrożenie dostaje katalog z datą, a nginx czyta zawsze dowiązanie current:

tekst
/var/www/example.pl/
├── releases/
│   ├── 20261110-210512/
│   └── 20261111-091733/
└── current -> releases/20261111-091733

Przygotuj strukturę:

bash
sudo mkdir -p /var/www/example.pl/releases
sudo chown -R deploy:deploy /var/www/example.pl

Dlaczego nie rsync prosto do katalogu strony? Kopiowanie trwa kilka sekund i w tym czasie odwiedzający może dostać nowy HTML ze starymi plikami CSS albo odwrotnie. Astro nadaje plikom w /_astro/ nazwy z hashem, więc stary HTML wskazujący na usunięty plik oznacza stronę bez stylów.

Krok 3. Konfiguracja nginx

W bloku server dla HTTPS (ten, który certbot uzupełnił o certyfikat) ustaw:

nginx
root /var/www/example.pl/current;
index index.html;
charset utf-8;

gzip on;
gzip_static on;
gzip_vary on;
gzip_types text/css text/javascript application/javascript application/json
           application/xml image/svg+xml text/plain;

error_page 404 /404.html;

location / {
    try_files $uri $uri/ $uri/index.html =404;
    add_header Cache-Control "no-cache";
}

location ^~ /_astro/ {
    add_header Cache-Control "public, max-age=31536000, immutable";
}

location ~* \.(?:png|jpe?g|webp|avif|gif|ico|woff2)$ {
    add_header Cache-Control "public, max-age=604800";
}

Co tu się dzieje:

  • no-cache dla HTML nie wyłącza pamięci podręcznej. Przeglądarka trzyma plik, ale przed użyciem pyta serwer, czy się zmienił (nginx odpowiada 304, jeśli nie). Dzięki temu po wdrożeniu nikt nie zobaczy starej wersji.
  • /_astro/ na rok z immutable – nazwa pliku zmienia się razem z treścią, więc stary plik nigdy nie dostanie nowej zawartości. Modyfikator ^~ jest potrzebny: bez niego nginx wybrałby dla obrazków i fontów z /_astro/ blok z wyrażeniem regularnym (7 dni), bo wyrażenia regularne mają pierwszeństwo przed zwykłym prefiksem.
  • Obrazki i fonty na 7 dni – rozsądny kompromis dla plików z public/, które nie mają hasha w nazwie. Rozsądny zestaw: HTML no-cache, /_astro/ rok, pliki json, xml i txt (sitemapy, robots.txt) godzinę, reszta statyki 7 dni.
  • gzip_types – text/html nginx kompresuje zawsze, resztę trzeba wymienić. gzip_vary on dodaje nagłówek Vary: Accept-Encoding dla pośredników.

Uwaga na pułapkę z dokumentacji nginx: add_header z bloku nadrzędnego jest dziedziczony tylko wtedy, gdy w bieżącym bloku nie ma żadnego add_header. Jeśli ustawiasz nagłówki bezpieczeństwa (HSTS, X-Content-Type-Options) na poziomie server, w każdym location z własnym add_header musisz je powtórzyć – najwygodniej przez include wspólnego pliku.

Sprawdź i przeładuj:

bash
sudo nginx -t && sudo systemctl reload nginx

Krok 4. Skrypt wdrożenia

Zapisz w projekcie jako deploy.sh:

bash
#!/usr/bin/env bash
set -euo pipefail

HOST=deploy@vps
APP=/var/www/example.pl
URL=https://example.pl/
KEEP=3
TS=$(date +%Y%m%d-%H%M%S)

npm ci
npm run build
test -f dist/index.html

# kompresja z wyprzedzeniem dla gzip_static
find dist -type f \( -name '*.html' -o -name '*.css' -o -name '*.js' \
  -o -name '*.svg' -o -name '*.xml' -o -name '*.json' -o -name '*.txt' \) \
  -exec gzip -k -9 {} +

PREV=$(ssh "$HOST" "readlink -e $APP/current || true")
ssh "$HOST" "mkdir -p $APP/releases/$TS"
rsync -a ${PREV:+--link-dest="$PREV"} dist/ "$HOST:$APP/releases/$TS/"

# atomowa podmiana: nowe dowiązanie obok, potem rename na miejsce starego
ssh "$HOST" "ln -sfn $APP/releases/$TS $APP/.current.new && mv -Tf $APP/.current.new $APP/current"

if ! curl -fsS -o /dev/null "$URL"; then
  echo "Strona nie odpowiada, wracam do $PREV" >&2
  [ -n "$PREV" ] && ssh "$HOST" "ln -sfn $PREV $APP/.current.new && mv -Tf $APP/.current.new $APP/current"
  exit 1
fi

ssh "$HOST" "cd $APP/releases && ls -1d */ | sort | head -n -$KEEP | xargs -r rm -rf"
echo "Wdrożono $TS"
bash
chmod +x deploy.sh
./deploy.sh

Trzy szczegóły, które robią różnicę:

  • ln -sfn + mv -Tf. Samo ln -sfn usuwa stare dowiązanie i tworzy nowe – przez ułamek sekundy current nie istnieje. Zmiana nazwy (mv -T, czyli rename) podmienia dowiązanie w jednej operacji systemu plików.
  • --link-dest sprawia, że pliki, które się nie zmieniły, są twardymi dowiązaniami do poprzedniego wydania – nie zajmują drugi raz miejsca i nie lecą przez sieć.
  • gzip -k -9 zostawia oryginał i tworzy obok .gz. Moduł gzip_static wyśle gotowy plik zamiast kompresować przy każdym żądaniu. Według dokumentacji nginx ten moduł nie jest wbudowany domyślnie; sprawdź, czy Twój pakiet go ma: nginx -V 2>&1 | grep -o with-http_gzip_static_module. Jeśli nie ma, usuń gzip_static on – zostanie zwykła kompresja w locie.

Krok 5. Cofnięcie wdrożenia

Lista wydań i powrót do poprzedniego:

bash
ssh deploy@vps 'ls -1 /var/www/example.pl/releases'
ssh deploy@vps 'cd /var/www/example.pl && ln -sfn releases/20261110-210512 .current.new && mv -Tf .current.new current'

nginx nie wymaga przeładowania – przy każdym żądaniu rozwiązuje ścieżkę na nowo. Wyjątek: jeśli włączyłeś open_file_cache, stara ścieżka może być używana do czasu wygaśnięcia wpisów.

Jeśli wdrożenia idą automatycznie z repozytorium, po ręcznym cofnięciu wycofaj też zmianę w repozytorium. Jeśli serwer buduje stronę po każdym pushu, bez tego następne wdrożenie przywróci błąd.

Krok 6. Sprawdzenie nagłówków

bash
curl -sI https://example.pl/ | grep -iE '^(HTTP|cache-control|content-encoding)'
curl -sI -H 'Accept-Encoding: gzip' https://example.pl/ | grep -i content-encoding
curl -sI https://example.pl/_astro/$(ssh deploy@vps 'ls /var/www/example.pl/current/_astro | head -1') | grep -i cache-control

Oczekujesz 200, cache-control: no-cache dla strony, content-encoding: gzip przy drugim zapytaniu i max-age=31536000, immutable dla pliku z /_astro/.

Najczęstsze problemy

  • Podstrony bez ukośnika na końcu zwracają 404. Brakuje $uri/index.html w try_files albo strona ma build.format: 'file'. Przy formacie katalogowym zostaw try_files jak w kroku 3.
  • Po wdrożeniu strona jest bez stylów. HTML z przeglądarki albo CDN wskazuje na plik z /_astro/, którego już nie ma. Sprawdź, czy HTML ma no-cache, a nie długi max-age – długi cache wolno dawać tylko plikom z hashem w nazwie.
  • Nagłówek HSTS zniknął z części odpowiedzi. To dziedziczenie add_header opisane w kroku 3. Powtórz nagłówki w każdym location z własnym add_header.
  • Permission denied przy rsync albo 403 z nginx. Katalog wydania należy do innego użytkownika albo nginx (www-data) nie może go czytać. ls -ld /var/www/example.pl/releases/* – katalogi powinny mieć prawa 755, pliki 644.
  • Dysk się zapełnia. Sprzątanie w skrypcie zostawia trzy najnowsze wydania. Jeśli wróciłeś ręcznie do starszego wydania, sprawdź przed sprzątaniem, czy current nie wskazuje na katalog spoza tej trójki.

Co dalej

Strona statyczna jest odtwarzalna z repozytorium, ale konfiguracja nginx i certyfikaty już nie – ustaw kopie zapasowe poza serwerem i monitoring z alertami. Jeśli przenosisz stronę ze zwykłego hostingu, zobacz migrację na VPS bez przestoju. Serwer pod kilka stron znajdziesz w dziale promocje: VPS.

Powiązane poradniki