SSD Nodes Learn Hosting plans →
Przewodniki Matt ConnorAutor: Matt Connor

VPS do testowania aplikacji: staging krok po kroku

Osobny VPS do testowania aplikacji: staging oddzielony od produkcji, własna baza i sekrety, blokada przed Google i botami, sensowny rozmiar planu oraz dane testowe zgodne z RODO.

Co należy na serwerze testowym, a co nie

VPS do testowania aplikacji to druga, tańsza maszyna, na której wolno wszystko zepsuć. Ma odtwarzać produkcję w tym, co wpływa na zachowanie kodu: ta sama distro, ta sama wersja języka i bazy danych, ten sam serwer WWW, ten sam zestaw migracji. Nie ma odtwarzać produkcji w tym, co dotyka prawdziwych ludzi i prawdziwych pieniędzy.

Na serwer testowy nie trafia produkcyjna baza z danymi klientów, nie trafiają produkcyjne hasła ani klucze API w trybie live, nie trafia konfiguracja poczty wysyłającej na prawdziwe adresy i nie trafia dostęp do produkcyjnych backupów. Każda z tych czterech rzeczy zamienia zwykłą pomyłkę w incydent. Skrypt uruchomiony na stagingu, który przez literówkę w DATABASE_URL wskazuje na produkcję, wyczyści produkcyjną tabelę bez jednego ostrzeżenia, bo baza nie wie, skąd przyszło polecenie. Rozdzielenie poświadczeń jest jedyną rzeczą, która to zatrzymuje.

Co należy: kod z dowolnej gałęzi, zanonimizowane dane o realistycznym rozmiarze, pełny stos usług (baza, cache, kolejka), ten sam reverse proxy i ten sam sposób uruchamiania procesów co na produkcji. Staging, który różni się sposobem startu aplikacji, nie sprawdza wdrożenia, tylko sam kod.

Drugi VPS czy druga instancja na tej samej maszynie

Dwa stosy na jednej maszynie są tańsze, ale dzielą jądro, dysk, pamięć i adres IP. To nie jest problem teoretyczny. Build obrazu Dockera na stagingu potrafi zająć cały wolny RAM, a wtedy jądro uruchamia OOM killera, który wybiera proces o największym zużyciu pamięci. Bardzo często jest to produkcyjny Postgres. W dmesg zostaje wpis Out of memory: Killed process 1234 (postgres), a Twoi klienci widzą błąd 500 z powodu testu, którego nie zamawiali.

Drugi powód jest bezpieczeństwowy. Na stagingu z definicji uruchamiasz niesprawdzony kod, czasem z gałęzi, której nikt nie przejrzał. Jeśli ten kod działa na tej samej maszynie co produkcja, każda luka w nim daje napastnikowi dostęp lokalny obok produkcyjnych plików i produkcyjnego gniazda bazy. Izolacja kontenerem pomaga, ale nie jest granicą, na której chcesz opierać dane klientów.

Dlatego dla aplikacji z prawdziwymi użytkownikami: osobny VPS, najtańszy plan. Jeśli mimo wszystko zostajesz na jednej maszynie, zrób minimum: osobny użytkownik systemowy, osobna sieć Dockera, osobny klaster bazy na innym porcie i twarde limity pamięci w docker compose, żeby test nie mógł zabrać zasobów produkcji.

Osobna baza i osobne sekrety

Staging dostaje własną bazę, własnego użytkownika bazy i własne hasło. Nie chodzi o porządek, chodzi o to, że hasło, którego staging nie zna, nie może zostać ze stagingu wyniesione.

sudo -u postgres createuser --pwprompt app_staging
sudo -u postgres createdb --owner=app_staging app_staging

Sekrety trzymaj w osobnym pliku i osobnej nakładce kompozycji. Plik compose.staging.yaml nadpisuje tylko to, co ma się różnić, więc reszta definicji zostaje wspólna z produkcją i nie rozjeżdża się po miesiącu.

services:
  app:
    env_file:
      - .env.staging
    environment:
      APP_ENV: staging
      MAIL_HOST: mailpit
      MAIL_PORT: "1025"
docker compose -f compose.yaml -f compose.staging.yaml config
docker compose -f compose.yaml -f compose.staging.yaml up -d

Polecenie config wypisuje scaloną konfigurację, czyli dokładnie to, co pójdzie w ruch. Warto je uruchomić raz i przeczytać, bo kolejność plików decyduje o wyniku: późniejszy plik wygrywa dla wartości skalarnych, ale listy potrafią się zachować inaczej niż zakładasz. Szczegóły scalania opisuje łączenie kilku plików docker compose w jedną konfigurację.

Jeżeli produkcyjny sekret kiedykolwiek wylądował na serwerze testowym, choćby w historii bash albo w logu CI, jest spalony. Traktuj go jak ujawniony i rotuj. Klucze płatności trzymaj w trybie testowym dostawcy, a webhooki przekierowuj na staging, nigdy nie odwrotnie.

Poczta: najczęstszy wyciek ze stagingu

Typowy wypadek wygląda tak: ktoś wgrywa kopię produkcyjnej bazy na staging, uruchamia kolejkę i cztery tysiące prawdziwych osób dostaje maila "Twoje zamówienie zostało wysłane". Przyczyna jest zawsze ta sama: w bazie są prawdziwe adresy, a konfiguracja poczty potrafi wyjść do internetu. Wystarczy usunąć jedną z tych dwóch rzeczy, ale usuń obie.

Najprostszy sposób to lokalna skrzynka przechwytująca. Mailpit przyjmuje SMTP na porcie 1025 i pokazuje wiadomości w przeglądarce na 8025, nic nie wysyłając dalej.

services:
  mailpit:
    image: axllent/mailpit:latest
    restart: unless-stopped
    ports:
      - "127.0.0.1:8025:8025"

Port panelu jest przypięty do 127.0.0.1, więc nie jest widoczny z internetu. Dostajesz się do niego tunelem SSH ze swojego komputera:

ssh -L 8025:127.0.0.1:8025 admin@staging.example.com

Drugi powód, żeby staging nie wysyłał poczty Twoją produkcyjną domeną: reputacja nadawcy. Odbicia i zgłoszenia spamu z testowych wysyłek liczą się do tej samej domeny, z której piszesz do klientów, więc jeden nieudany test kolejki pogarsza dostarczalność faktur.

Subdomena, certyfikat i pułapka z basic auth

Staging dostaje własną nazwę, na przykład staging.example.com, z osobnym rekordem A. Certyfikat wystawiasz normalnie:

sudo apt update && sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d staging.example.com

Tu pojawia się błąd, który zaskakuje prawie każdego. Jeśli najpierw założysz hasło HTTP na cały serwer, walidacja ACME (automatic certificate management environment, czyli protokół wystawiania certyfikatów) nie przejdzie, bo serwer odpowiada urzędowi certyfikacji żądaniem logowania zamiast plikiem wyzwania. Certbot pokazuje wtedy komunikat w postaci Invalid response from http://staging.example.com/.well-known/acme-challenge/.... Rozwiązanie to jeden wyjątek w konfiguracji, wstawiony przed regułą z hasłem:

location ^~ /.well-known/acme-challenge/ {
    auth_basic off;
    allow all;
    root /srv/acme;
}

Prefiks ^~ sprawia, że to dopasowanie wygrywa z wyrażeniami regularnymi, a auth_basic off wyłącza odziedziczone hasło tylko dla tej ścieżki.

Jak trzymać staging poza Google i poza ciekawskimi

Trzy mechanizmy, każdy robi inną robotę. Hasło HTTP nie wpuszcza nikogo bez loginu. Nagłówek X-Robots-Tag mówi wyszukiwarce, żeby nie trzymała adresu w indeksie. Lista dozwolonych adresów IP odcina resztę świata na poziomie sieci. Plik robots.txt nie jest żadnym z tych mechanizmów: to prośba o niepobieranie strony, a nie polecenie usunięcia adresu, który wyszukiwarka zna z innego źródła.

sudo apt install -y apache2-utils
sudo htpasswd -c /etc/nginx/.htpasswd klient
sudo chown root:www-data /etc/nginx/.htpasswd
sudo chmod 640 /etc/nginx/.htpasswd
server {
    listen 443 ssl;
    server_name staging.example.com;

    auth_basic "staging";
    auth_basic_user_file /etc/nginx/.htpasswd;

    add_header X-Robots-Tag "noindex, nofollow" always;

    location / {
        add_header X-Robots-Tag "noindex, nofollow" always;
        proxy_pass http://127.0.0.1:3000;
        proxy_set_header Host $host;
    }
}

Dwie rzeczy w tym pliku wyglądają na zbędne, a nie są. Parametr always dokłada nagłówek również do odpowiedzi błędnych, więc pojawia się on także przy odmowie logowania, a to jest dokładnie ta odpowiedź, którą zobaczy robot. Powtórzenie add_header w bloku location jest potrzebne, bo nginx nie scala nagłówków z poziomu wyżej: własny add_header w bloku zagnieżdżonym unieważnia wszystkie odziedziczone.

Po zmianie sprawdź konfigurację i obejrzyj nagłówki samodzielnie:

sudo nginx -t
sudo systemctl reload nginx
curl -I https://staging.example.com/
curl -I -u klient https://staging.example.com/

Pierwsze wywołanie, bez loginu, nie powinno pokazać treści aplikacji. Drugie, po podaniu hasła, powinno ją pokazać. W obu odpowiedziach szukaj wiersza x-robots-tag. Jeśli widzisz go tylko w jednej, to znaczy, że zapomniałeś o always albo o powtórzeniu nagłówka w location.

Lista adresów IP to osobna warstwa i domyślnie łączy się z hasłem spójnikiem "oraz". Jeżeli chcesz, żeby wystarczył jeden z dwóch warunków, powiedz to wprost:

    satisfy any;
    allow 203.0.113.10;
    deny all;

Bez satisfy any klient z hasłem i innym adresem nie wejdzie. Pamiętaj, że domowe łącze zwykle ma zmienny adres, więc sama lista IP potrafi zablokować osobę, dla której postawiłeś podgląd. Na poziomie systemu zamknij wszystko poza tym, co potrzebne:

sudo ufw default deny incoming
sudo ufw allow from 203.0.113.10 to any port 22 proto tcp
sudo ufw allow 443/tcp
sudo ufw enable

Dane testowe, RODO i odpowiedzialność za serwer testowy

Skopiowanie produkcyjnej bazy na serwer testowy jest przetwarzaniem danych osobowych, a nie czynnością techniczną. Obowiązują te same zasady co na produkcji: minimalizacja danych z art. 5 ust. 1 lit. c RODO mówi, że przetwarzasz tylko dane niezbędne do celu, a art. 32 wymaga odpowiednich zabezpieczeń. Testowanie zwykle nie jest celem, w jakim zebrałeś adresy i numery telefonów klientów, więc pełna kopia produkcji na stagingu to przetwarzanie do nowego celu, które trzeba uzasadnić, a nie domyślny krok w procesie wdrożenia.

Uczciwa praktyka jest prosta: dane syntetyczne albo zanonimizowane. Warto wiedzieć, że pseudonimizacja nie wyprowadza zbioru poza RODO, bo dane nadal da się powiązać z osobą przy użyciu dodatkowych informacji. Anonimizacja wyprowadza, ale tylko wtedy, gdy jest nieodwracalna, czyli gdy nie zostawiasz w bazie tabeli mapującej stare wartości na nowe. Najbezpieczniejszy moment na podmianę to eksport, nie import, bo dane, które nigdy nie wylądowały na serwerze testowym, nie mogą z niego wyciec.

UPDATE users
SET email = 'user' || id || '@example.invalid',
    phone = NULL,
    last_name = 'Testowy',
    pesel = NULL;

Domena example.invalid jest zarezerwowana i nigdy się nie rozwiązuje, więc nawet źle skonfigurowany mailer nie dostarczy do nikogo wiadomości. To dlatego lepiej jej użyć niż wymyślonej nazwy, która może komuś naprawdę należeć.

Dalej: serwer testowy jest infrastrukturą, za którą odpowiada firma. Hosting jest podmiotem przetwarzającym, więc potrzebna jest umowa powierzenia obejmująca także tę maszynę, a czynność powinna być widoczna w rejestrze czynności przetwarzania i w analizie ryzyka. Naruszenie na stagingu z prawdziwymi danymi jest naruszeniem jak każde inne, z 72 godzinami na zgłoszenie do UODO. To nie jest porada prawna, a interpretacje bywają różne, więc konkretny przypadek skonsultuj z inspektorem ochrony danych albo prawnikiem. Techniczny wniosek jest jednak niezależny od interpretacji: baza bez prawdziwych danych osobowych usuwa cały ten problem u źródła.

Ile VPS-a potrzebuje serwer testowy

Serwer testowy stoi bezczynnie większość dnia, więc najtańszy plan zwykle wystarcza. Dla jednej aplikacji z Postgresem i nginx za reverse proxy punktem wyjścia jest 1 vCPU, 2 GB RAM i 20 do 40 GB dysku. Jeśli budujesz obrazy Dockera na tej samej maszynie albo uruchamiasz testy równolegle, celuj w 2 vCPU i 4 GB, bo to build, a nie ruch, wyznacza szczyt zużycia. Ceny zmieniają się częściej niż ten tekst (stan na wrzesień 2026), więc sprawdź aktualny cennik, zamiast przyjmować liczbę z artykułu. Jak dopasować plan do konkretnego profilu obciążenia, rozbiera dobieranie rozmiaru VPS pod typowe zadania, a przegląd tego, co jeszcze zmieści się na jednej takiej maszynie, znajdziesz w zestawieniu zastosowań własnego VPS.

Na małym planie dodaj swap. Bez niego szczyt pamięci podczas budowania obrazu kończy się zabiciem procesu, a z nim kończy się chwilowym spowolnieniem.

sudo fallocate -l 2G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
swapon --show

Wpis w /etc/fstab sprawia, że swap wróci po restarcie, bo swapon sam z siebie nie przetrwa reboota.

Pilnuj dysku, bo to on kończy się pierwszy. Stare obrazy, wolumeny po skasowanych gałęziach i logi aplikacji rosną cicho. docker system prune -af zwalnia obrazy i warstwy budowania, ale dopisanie --volumes usuwa też dane baz, więc na stagingu z przygotowanym zbiorem testowym używaj go świadomie. Jeśli df twierdzi, że dysk jest pełny, a du nie potrafi znaleźć winnego, przyczyna jest zwykle inna niż pliki, które widzisz, i opisuje ją rozbieżność między df i du przy pełnym dysku.

Podgląd gałęzi dla klienta

Jedna maszyna obsłuży kilka równoległych podglądów, jeśli oddzielisz projekty kompozycji. Nazwa projektu decyduje o nazwach kontenerów, sieci i wolumenów, więc dwie gałęzie z różnymi nazwami nie nadpiszą sobie bazy.

COMPOSE_PROJECT_NAME=pr-142 docker compose -f compose.yaml -f compose.staging.yaml up -d

Kontenery nie publikują wtedy portów na stałych numerach, bo dwa naraz nie zmieszczą się na tym samym porcie. Zostaw usługi w sieci wewnętrznej i skieruj na nie ruch z nginx po nazwie hosta, na przykład pr-142.staging.example.com. Certyfikat wieloznaczny na *.staging.example.com wymaga walidacji przez DNS, więc potrzebujesz wtyczki certbota do swojego operatora DNS. Jeśli nie chcesz tego stawiać, wystaw certyfikat dla każdej nazwy osobno i skasuj go razem z gałęzią.

Kiedy staging kłamie

Staging jest przydatny dokładnie w takim stopniu, w jakim jest podobny do produkcji. Cztery różnice, które najczęściej ukrywają błąd: inna wersja minor bazy danych, włączony tryb debug, inna strefa czasowa systemu i zbyt mały zbiór danych. Ostatnia jest najbardziej zdradliwa. Migracja, która na pięciuset wierszach kończy się w dwie sekundy, na kilku milionach potrafi założyć blokadę na tabeli na kilka minut i zatrzymać produkcję. Dlatego migracje ryzykowne rehearsuj na zanonimizowanej kopii o realistycznym rozmiarze, a nie na zbiorze z seedera.

Dlatego też wersje przypinaj jawnie w obu środowiskach. Tag latest w obrazie oznacza, że staging i produkcja mogą pracować na różnym kodzie, choć plik wygląda identycznie, a wtedy test niczego nie dowodzi.

Dyscyplina kasowania, czyli co czyni testowy VPS tanim

Serwer testowy jest tani wtedy, gdy potrafisz go zniszczyć i odtworzyć. Jeśli odtworzenie zajmuje godzinę klikania, nie zniszczysz go nigdy, a wtedy przestaje być narzędziem i staje się drugim systemem do utrzymywania, z własnymi aktualizacjami, własnymi certyfikatami i własnymi niespodziankami.

Rytm jest prosty. Przed ryzykowną próbą zrób snapshot u operatora. Po próbie wróć do snapshotu albo skasuj maszynę. Raz na jakiś czas odtwórz staging od zera z tego samego opisu, z którego powstaje produkcja, bo to jedyny test, który dowodzi, że ten opis jest nadal prawdziwy. Sensownym miejscem na ten opis jest konfiguracja cloud-init na nowym VPS, która wykonuje się przy pierwszym starcie i nie wymaga żadnego dodatkowego narzędzia.

Maszyna testowa, która działa nieprzerwanie od roku, prawie zawsze ma na sobie zmiany zrobione ręcznie i nieopisane nigdzie w repozytorium. Taki serwer nie odpowiada już produkcji, więc zielony wynik na nim nie znaczy nic. Kasowanie nie jest sprzątaniem. Jest testem tego, czy Twoje wdrożenie da się powtórzyć.

FAQ

Czy staging musi stać na osobnym VPS?

Nie musi, ale osobna maszyna usuwa dwa konkretne ryzyka. Pierwsze to pamięć: build na stagingu potrafi wywołać OOM killera, który zabija najbardziej pamięciożerny proces na maszynie, często produkcyjną bazę, co widać w dmesg jako Out of memory: Killed process. Drugie to bezpieczeństwo: na stagingu uruchamiasz nieprzejrzany kod, a na wspólnej maszynie jego luka leży obok produkcyjnych plików. Jeśli zostajesz na jednej maszynie, daj stagingowi osobnego użytkownika, osobną sieć kontenerów i twarde limity pamięci.

Czy mogę skopiować produkcyjną bazę na serwer testowy?

Technicznie tak, prawnie to przetwarzanie danych osobowych do nowego celu i podlega RODO, w tym zasadzie minimalizacji z art. 5 i wymogom bezpieczeństwa z art. 32. Praktyka, która nie wymaga tłumaczenia się, to dane syntetyczne albo nieodwracalnie zanonimizowane, podmieniane na etapie eksportu, a nie po wgraniu na staging. Pamiętaj, że pseudonimizacja nie wyprowadza zbioru poza RODO, bo powiązanie z osobą nadal jest możliwe. Konkretny przypadek skonsultuj z inspektorem ochrony danych.

Jak sprawdzić, że staging nie trafi do Google?

Załóż hasło HTTP w nginx i dodaj add_header X-Robots-Tag "noindex, nofollow" always;, a potem obejrzyj odpowiedź poleceniem curl -I https://staging.example.com/ i poszukaj wiersza x-robots-tag. Parametr always jest konieczny, bo bez niego nagłówek nie pojawia się na odpowiedzi z odmową logowania, czyli na tej, którą dostaje robot. Jeśli w bloku location masz własny add_header, powtórz w nim ten nagłówek, ponieważ nginx nie dziedziczy nagłówków do bloku, który definiuje swoje własne. Sam robots.txt nie wystarczy: to prośba o niepobieranie strony, a nie polecenie usunięcia adresu z indeksu.

Jaki plan VPS wystarczy na serwer testowy?

Dla jednej aplikacji z bazą i reverse proxy punktem wyjścia jest 1 vCPU, 2 GB RAM i 20 do 40 GB dysku, bo taka maszyna stoi bezczynnie przez większość dnia. Do budowania obrazów Dockera albo równoległych testów weź 2 vCPU i 4 GB, bo szczyt zużycia wyznacza build, nie ruch użytkowników. Na małym planie dodaj plik swap, żeby szczyt pamięci kończył się spowolnieniem, a nie zabiciem procesu. Ceny i parametry planów zmieniają się, więc sprawdź aktualny cennik operatora przed zamówieniem.