Przejdź do treści
·12 min czytania

Konfiguracja VPS, która naprawdę działa: Node.js, PM2, Nginx i wdrożenia bez przestojów

Dokładna konfiguracja wdrożeniowa VPS, której używam na produkcji — hardening Ubuntu, tryb cluster PM2, reverse proxy Nginx, SSL i skrypt deploy, który mnie jeszcze nie zawiódł. Bez teorii, tylko to, co działa.

Udostępnij:X / TwitterLinkedIn

Ten blog działa na VPS za 10$/miesiąc. Nie Vercel, nie AWS, nie klaster Kubernetes zarządzany przez sześcioosobowy zespół. Pojedynczy serwer Ubuntu z Nginx, PM2 i skryptem bash, który wdraża w mniej niż 30 sekund.

Próbowałem innych ścieżek. Używałem Vercel (świetny, dopóki nie potrzebujesz zadań cron, trwałych WebSocketów, albo po prostu kontroli). Używałem AWS (świetny, jeśli lubisz spędzać pół dnia w politykach IAM). Zawsze wracam do VPS.

Ale jest problem: każdy poradnik "wdróż na VPS" w internecie kończy się na happy path. Pokazują ci, jak zainstalować Node.js i uruchomić node server.js, i nazywają to produkcją. Potem twój serwer zostaje zbrute-force'owany przez SSH, twój proces umiera o 3 w nocy, bo nikt nie skonfigurował menedżera procesów, a twój certyfikat SSL wygasł trzy miesiące temu.

To jest poradnik, którego chciałbym wtedy mieć. Wszystko tutaj jest przetestowane w boju — dokładnie ta konfiguracja serwuje stronę, którą teraz czytasz.

Zacznij od bezpieczeństwa, nie od kodu#

Zanim w ogóle pomyślisz o Node.js, zabezpiecz maszynę. Świeże instancje VPS są celami. Zautomatyzowane boty zaczynają atakować twój port SSH w ciągu minut od uruchomienia.

Utwórz użytkownika bez uprawnień root#

bash
adduser deploy
usermod -aG sudo deploy

Skonfiguruj uwierzytelnianie kluczem SSH#

Na twoim lokalnym komputerze:

bash
ssh-copy-id deploy@your-server-ip

Następnie wyłącz uwierzytelnianie hasłem całkowicie:

bash
sudo nano /etc/ssh/sshd_config
bash
PasswordAuthentication no
PermitRootLogin no
bash
sudo systemctl restart sshd

Jeśli to pominiesz, zobaczysz tysiące nieudanych prób logowania w swoich logach auth w ciągu kilku dni. To nie jest paranoja — to zwykły wtorek w publicznym internecie.

Firewall z UFW#

bash
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
sudo ufw enable

To wszystko. Cztery reguły. Tylko ruch SSH i webowy przechodzą.

Fail2Ban#

bash
sudo apt install fail2ban -y
sudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local

Edytuj /etc/fail2ban/jail.local:

ini
[sshd]
enabled = true
port = ssh
filter = sshd
logpath = /var/log/auth.log
maxretry = 3
bantime = 3600
findtime = 600
bash
sudo systemctl enable fail2ban
sudo systemctl start fail2ban

Trzy nieudane próby SSH i jesteś zbanowany na godzinę. Obserwowałem, jak Fail2Ban blokował setki adresów IP w ciągu jednego dnia. To działa.

Automatyczne aktualizacje bezpieczeństwa#

bash
sudo apt install unattended-upgrades -y
sudo dpkg-reconfigure -plow unattended-upgrades

Twój serwer będzie teraz automatycznie instalować poprawki bezpieczeństwa. Jedna rzecz mniej do zapamiętania.

Node.js: użyj NVM, nie apt#

Widzę to w każdym poradniku: sudo apt install nodejs. Nie rób tego.

Repozytoria pakietów Ubuntu dostarczają starożytne wersje Node.js. Nawet PPA NodeSource pozostaje w tyle. A kiedy musisz przełączać się między Node 20 a Node 22 dla różnych projektów, utkniesz.

NVM rozwiązuje ten problem:

bash
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash
source ~/.bashrc
nvm install --lts
nvm alias default lts/*

Teraz zweryfikuj:

bash
node -v  # v22.x.x lub aktualna wersja LTS
npm -v

Nieoczywista wskazówka: kiedy instalujesz globalne pakiety z NVM (jak PM2), są one powiązane z tą wersją Node. Jeśli przełączysz wersję za pomocą nvm use, twoje pakiety globalne znikają. Ustaw domyślną i trzymaj się jej na serwerze:

bash
nvm alias default 22

To ugryzło mnie dokładnie raz. Raz wystarczyło.

PM2: menedżer procesów, który zasługuje na swoje miejsce#

PM2 to różnica między "wdrożonym" a "gotowym na produkcję". Obsługuje zarządzanie procesami, klastrowanie, rotację logów, auto-restart po crashach i skrypty startowe. Za darmo.

Instalacja i konfiguracja#

bash
npm install -g pm2

Konfiguracja ecosystem#

Nie uruchamiaj aplikacji flagami CLI. Użyj pliku ecosystem.config.js. Jest wersjonowany, powtarzalny i samodokumentujący się.

javascript
// ecosystem.config.js
module.exports = {
  apps: [
    {
      name: "akousa",
      script: "node_modules/.bin/next",
      args: "start -p 3002",
      cwd: "/var/www/akousa.net",
      instances: 2,
      exec_mode: "cluster",
      max_memory_restart: "500M",
      env: {
        NODE_ENV: "production",
        PORT: 3002,
      },
      // Graceful shutdown
      kill_timeout: 5000,
      listen_timeout: 10000,
      wait_ready: false,
      // Logowanie
      log_date_format: "YYYY-MM-DD HH:mm:ss Z",
      error_file: "/var/log/pm2/akousa-error.log",
      out_file: "/var/log/pm2/akousa-out.log",
      merge_logs: true,
      // Auto-restart przy awarii
      autorestart: true,
      max_restarts: 10,
      min_uptime: "10s",
      // Nie obserwuj zmian na produkcji
      watch: false,
    },
  ],
};

Pozwólcie, że wyjaśnię wybory, które mają znaczenie:

instances: 2 zamiast "max": Na małym VPS z 1-2 rdzeniami "max" brzmi mądrze, ale uruchomi procesy, które walczą o zasoby podczas buildów. Dwie instancje dają ci przeładowania bez przestojów, zostawiając zapas. Na maszynie z 4+ rdzeniami, jasne, użyj "max".

exec_mode: "cluster": To jest to, co umożliwia przeładowania bez przestojów. Bez trybu cluster pm2 reload to tylko ozdobny restart. Z trybem cluster PM2 restartuje instancje jedna po drugiej — twoja aplikacja nigdy nie jest w pełni offline.

max_memory_restart: "500M": Twoja aplikacja Next.js ma wyciek pamięci? PM2 zrestartuje ją, zanim OOM-kill położy twój serwer. To uratowało mnie od alertów o 2 w nocy nie raz.

kill_timeout: 5000: Daje twojej aplikacji 5 sekund na dokończenie aktualnych żądań, zanim PM2 ją ubije na siłę. Domyślna wartość (1600ms) jest zbyt agresywna dla aplikacji z połączeniami do bazy danych.

watch: false: Widziałem ludzi, którzy zostawiali watch: true na produkcji. PM2 wtedy restartuje aplikację za każdym razem, gdy zmienia się plik logów. Twoja aplikacja wchodzi w pętlę restartów. Nie rób tego.

Skrypt startowy#

Spraw, by PM2 przetrwał restarty serwera:

bash
pm2 startup systemd
# Skopiuj i uruchom komendę, którą wyświetli
pm2 save

To generuje usługę systemd. Po restarcie serwera twoja aplikacja wraca automatycznie. Przetestuj to — zrestartuj serwer i zweryfikuj. Nie zakładaj.

Rotacja logów#

Logi w końcu zjedzą twój dysk. Zainstaluj moduł rotacji:

bash
pm2 install pm2-logrotate
pm2 set pm2-logrotate:max_size 50M
pm2 set pm2-logrotate:retain 7
pm2 set pm2-logrotate:compress true

Maks. 50MB na plik, zachowaj 7 zrotowanych plików, kompresuj stare. Bez tego widziałem, jak /var/log zapełniało 25GB dysk w trzy tygodnie na umiarkowanie obciążonej aplikacji.

Nginx: reverse proxy, który robi więcej niż myślisz#

"Dlaczego po prostu nie wystawić Node.js bezpośrednio na porcie 80?"

Ponieważ Nginx obsługuje rzeczy, na które Node.js nie powinien marnować cykli: terminację SSL, serwowanie plików statycznych, kompresję gzip, buforowanie żądań, limity połączeń i graceful handling wolnych klientów. Jest napisany w C i stworzony specjalnie do tego.

Instalacja#

bash
sudo apt install nginx -y

Konfiguracja#

nginx
# /etc/nginx/sites-available/akousa.net
 
upstream node_app {
    server 127.0.0.1:3002;
    keepalive 64;
}
 
server {
    listen 80;
    listen [::]:80;
    server_name akousa.net www.akousa.net;
 
    # Przekieruj cały HTTP na HTTPS
    return 301 https://$host$request_uri;
}
 
server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name akousa.net www.akousa.net;
 
    # SSL (zarządzany przez Certbot — te linie dodawane są automatycznie)
    ssl_certificate /etc/letsencrypt/live/akousa.net/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/akousa.net/privkey.pem;
    include /etc/letsencrypt/options-ssl-nginx.conf;
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
 
    # Nagłówki bezpieczeństwa
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-XSS-Protection "1; mode=block" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;
    add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
 
    # Kompresja gzip
    gzip on;
    gzip_vary on;
    gzip_proxied any;
    gzip_comp_level 6;
    gzip_min_length 256;
    gzip_types
        text/plain
        text/css
        text/javascript
        application/javascript
        application/json
        application/xml
        image/svg+xml
        application/wasm;
 
    # Ustawienia proxy
    location / {
        proxy_pass http://node_app;
        proxy_http_version 1.1;
 
        # Nagłówki
        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;
 
        # Obsługa WebSocket (jeśli kiedykolwiek będziesz potrzebować)
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
 
        # Timeouty — hojne, ale nie nieskończone
        proxy_connect_timeout 60s;
        proxy_send_timeout 60s;
        proxy_read_timeout 60s;
 
        # Buforowanie — niech Nginx obsługuje wolnych klientów
        proxy_buffering on;
        proxy_buffer_size 16k;
        proxy_buffers 4 32k;
        proxy_busy_buffers_size 64k;
    }
 
    # Statyczne zasoby Next.js — niech Nginx serwuje je bezpośrednio
    location /_next/static/ {
        alias /var/www/akousa.net/.next/static/;
        expires 365d;
        access_log off;
        add_header Cache-Control "public, immutable";
    }
 
    # Publiczne pliki statyczne
    location /static/ {
        alias /var/www/akousa.net/public/static/;
        expires 30d;
        access_log off;
    }
 
    # Blokuj dostęp do plików z kropką
    location ~ /\. {
        deny all;
        access_log off;
        log_not_found off;
    }
}

Włącz konfigurację:

bash
sudo ln -s /etc/nginx/sites-available/akousa.net /etc/nginx/sites-enabled/
sudo rm /etc/nginx/sites-enabled/default
sudo nginx -t
sudo systemctl reload nginx

Zawsze uruchamiaj nginx -t przed przeładowaniem. Kiedyś wrzuciłem uszkodzoną konfigurację i położyłem stronę, bo pominąłem sprawdzenie składni. Pięć znaków nginx -t zaoszczędziłoby mi trzydziestu minut panicznego debugowania.

Rzeczy, które większość poradników pomija w tej konfiguracji:

Blok upstream z keepalive 64: Nginx ponownie używa połączeń do twojego backendu Node.js zamiast otwierać nowe połączenie TCP dla każdego żądania. To ma znaczenie pod obciążeniem.

proxy_buffering on: Nginx odczytuje całą odpowiedź z Node.js do pamięci, a następnie wysyła ją klientowi z prędkością, jaką klient jest w stanie obsłużyć. Bez tego wolny klient na połączeniu 3G blokuje twój worker Node.js.

Serwowanie _next/static/ bezpośrednio: To są zahashowane, niemutowalne zasoby. Niech Nginx serwuje je z dysku z 365-dniowym nagłówkiem cache. Twoje procesy Node.js nie powinny na to marnować czasu.

SSL w pięć minut#

Let's Encrypt rozwiązał SSL. Jeśli nadal płacisz za certyfikaty w 2026 roku, przestań.

bash
sudo apt install certbot python3-certbot-nginx -y
sudo certbot --nginx -d akousa.net -d www.akousa.net

Certbot zapyta o twój email, zaakceptuje regulamin i automatycznie zmodyfikuje twoją konfigurację Nginx, dodając dyrektywy SSL. To wszystko.

Zweryfikuj automatyczne odnawianie#

Certbot instaluje timer systemd, który sprawdza dwa razy dziennie i odnawia certyfikaty w ciągu 30 dni przed wygaśnięciem:

bash
sudo systemctl list-timers | grep certbot

Przetestuj, czy odnawianie działa:

bash
sudo certbot renew --dry-run

Jeśli próbny przebieg się powiedzie, nigdy więcej nie pomyślisz o SSL. Jeśli się nie powiedzie, to zwykle dlatego, że port 80 jest zablokowany (sprawdź reguły UFW) lub Nginx nie działa.

Jedna rzecz, która mnie zaskoczyła: jeśli skonfigurujesz Nginx przed uruchomieniem Certbot, upewnij się, że twój blok server nasłuchuje na porcie 80 bez przekierowania HTTPS na początku. Certbot musi dotrzeć do portu 80 dla wyzwania HTTP-01. Po pomyślnym uruchomieniu Certbot wtedy dodaj przekierowanie.

Skrypt wdrożeniowy#

To jest skrypt, który uruchamiam za każdym razem, gdy wdrażam na produkcję. Żadnej platformy CI/CD, żadnych GitHub Actions. Tylko SSH i bash.

bash
#!/bin/bash
# deploy.sh — wdrożenie z (prawie) zerowym przestojem
 
set -euo pipefail
 
APP_DIR="/var/www/akousa.net"
APP_NAME="akousa"
LOG_FILE="/var/log/deploy.log"
HEALTH_URL="http://localhost:3002"
MAX_RETRIES=10
RETRY_INTERVAL=3
 
log() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
}
 
log "=== Deploy started ==="
 
cd "$APP_DIR"
 
# Pobierz najnowszy kod
log "Pulling latest changes..."
git pull origin main 2>&1 | tee -a "$LOG_FILE"
 
# Zainstaluj zależności
log "Installing dependencies..."
npm install --legacy-peer-deps 2>&1 | tee -a "$LOG_FILE"
 
# Zbuduj
log "Building application..."
rm -rf .next
npm run build 2>&1 | tee -a "$LOG_FILE"
 
if [ $? -ne 0 ]; then
    log "ERROR: Build failed. Aborting deploy."
    exit 1
fi
 
# Przeładuj PM2 (zero przestojów w trybie cluster)
log "Reloading PM2..."
pm2 reload "$APP_NAME" 2>&1 | tee -a "$LOG_FILE"
pm2 save 2>&1 | tee -a "$LOG_FILE"
 
# Sprawdzenie zdrowia z powtórzeniami
log "Running health check..."
for i in $(seq 1 $MAX_RETRIES); do
    HTTP_CODE=$(curl -s -o /dev/null -w '%{http_code}' "$HEALTH_URL" 2>/dev/null || echo "000")
    if [ "$HTTP_CODE" = "200" ]; then
        log "Health check passed (HTTP $HTTP_CODE)"
        log "=== Deploy completed successfully ==="
        exit 0
    fi
    log "Health check attempt $i/$MAX_RETRIES (HTTP $HTTP_CODE). Retrying in ${RETRY_INTERVAL}s..."
    sleep $RETRY_INTERVAL
done
 
log "ERROR: Health check failed after $MAX_RETRIES attempts"
log "Rolling back to previous PM2 state..."
pm2 restart "$APP_NAME" 2>&1 | tee -a "$LOG_FILE"
exit 1

Nadaj mu uprawnienia do wykonywania:

bash
chmod +x deploy.sh

Wdróż z lokalnego komputera:

bash
ssh root@your-server-ip "bash /var/www/akousa.net/deploy.sh"

Kluczowe decyzje w tym skrypcie:

set -euo pipefail: Skrypt natychmiast kończy działanie przy jakimkolwiek błędzie. Bez tego nieudany npm install cicho kontynuuje do kroku budowania, a ty dostajesz tajemniczy błąd, którego debugowanie zabiera 20 minut.

rm -rf .next przed budowaniem: Next.js ma cache builda, który czasami produkuje nieaktualne dane wyjściowe. Raz mnie to ugryzło — strona pokazywała stare treści mimo zaktualizowanego kodu źródłowego. Usunięcie katalogu builda dodaje może 15 sekund do budowania, ale gwarantuje świeże dane wyjściowe.

pm2 reload zamiast pm2 restart: To jest część z zerowym przestojem. W trybie cluster reload wykonuje rolling restart — uruchamia nowe instancje z zaktualizowanym kodem, czeka aż będą gotowe, następnie gracefully wyłącza stare. W żadnym momencie nie ma zero działających instancji.

Sprawdzenie zdrowia z powtórzeniami: Next.js potrzebuje kilku sekund na rozgrzewkę po restarcie. Skrypt czeka do 30 sekund (10 powtórzeń x 3 sekundy), sprawdzając czy aplikacja odpowiada kodem HTTP 200. Jeśli nie odpowiada, coś jest nie tak i musisz o tym wiedzieć natychmiast — nie dowiedzieć się od użytkownika.

Rollback przy błędzie: Jeśli sprawdzenie zdrowia nie powiedzie się po wszystkich powtórzeniach, skrypt restartuje PM2 (który ładuje ostatni zapisany stan). To nie jest idealny rollback, ale jest lepszy niż zostawienie serwera w uszkodzonym stanie.

Kiedy coś się psuje o 2 w nocy#

Oto co faktycznie debugowałem na dokładnie tej konfiguracji:

"Strona nie działa"#

Pierwsze komendy do uruchomienia:

bash
pm2 status
pm2 logs akousa --lines 50
sudo systemctl status nginx
sudo tail -50 /var/log/nginx/error.log

W dziewięciu na dziesięć przypadków pm2 logs natychmiast ci mówi, co się stało. Brakująca zmienna środowiskowa, nieudane połączenie z bazą danych lub nieobsłużona odrzucona obietnica.

"Pamięć ciągle rośnie"#

bash
pm2 monit

To daje ci live dashboard procesora i pamięci na proces. Jeśli pamięć stale rośnie bez stabilizacji, masz wyciek. Ustawienie max_memory_restart w konfiguracji ecosystem to twoja siatka bezpieczeństwa — PM2 zrestartuje proces, zanim położy serwer.

Dla głębszego zbadania:

bash
pm2 describe akousa

To pokazuje uptime, liczbę restartów i snapshoty pamięci. Jeśli widzisz 47 restartów w ciągu ostatnich 24 godzin, to twoja podpowiedź.

"Certyfikat SSL wygasł"#

bash
sudo certbot certificates

Wyświetla wszystkie certyfikaty z datami wygaśnięcia. Jeśli automatyczne odnawianie zawiodło:

bash
sudo certbot renew --force-renewal
sudo systemctl reload nginx

"Dysk jest pełny"#

bash
df -h
du -sh /var/log/*
pm2 flush

pm2 flush natychmiast czyści wszystkie pliki logów PM2. Jeśli nie skonfigurowałeś rotacji logów (mówiłem ci), tu odczujesz ból.

Komenda, którą uruchamiam każdego ranka#

bash
ssh deploy@akousa.net "pm2 status && df -h / && uptime"

Trzy rzeczy w jednej linii: czy moje procesy działają, czy dysk jest w porządku, czy serwer jest przeciążony. Zajmuje dwie sekundy. Wyłapuje problemy, zanim zrobią to użytkownicy.

Czego większość poradników ci nie powie#

Twój krok budowania to twoja największa podatność. Na VPS z 1GB RAM npm run build dla aplikacji Next.js może zużyć 800MB+ pamięci. Jeśli PM2 uruchamia twoją aplikację w dwóch instancjach podczas budowania, dostaniesz OOM. Rozwiązania: użyj swap file (co najmniej 2GB) albo zatrzymaj aplikację na czas budowania i zaakceptuj kilka sekund przestoju. Ja używam swap.

bash
sudo fallocate -l 2G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab

--legacy-peer-deps w komendzie instalacji to code smell, nie rozwiązanie. Używam tego, bo niektóre pakiety w moim drzewie zależności nie zaktualizowały swoich zakresów peer dependency. Co kilka miesięcy próbuję to usunąć. Kiedyś zadziała. Do tego czasu dostarczam.

Przetestuj swój skrypt wdrożeniowy od zera. Sklonuj swoje repozytorium na świeżym serwerze i uruchom każdy krok ręcznie. Liczba problemów typu "działa na mojej maszynie", które ukrywają się w skryptach wdrożeniowych, jest zawstydzająca. Znalazłem trzy problemy w swoim, kiedy to zrobiłem — brakujące pakiety globalne, złe uprawnienia plików i ścieżka, która istniała tylko z powodu poprzedniej ręcznej konfiguracji.

Wpisz IP swojego serwera do konfiguracji SSH. Przestań wpisywać adresy IP:

bash
# ~/.ssh/config
Host akousa
    HostName 69.62.66.94
    User deploy
    IdentityFile ~/.ssh/id_ed25519

Teraz ssh akousa to wszystko, czego potrzebujesz. Małe rzeczy się kumulują.

Pełna lista kontrolna#

Zanim uznasz, że skończyłeś:

  • Użytkownik bez uprawnień root z dostępem sudo
  • Tylko uwierzytelnianie kluczem SSH, uwierzytelnianie hasłem wyłączone
  • UFW włączony z otwartymi tylko niezbędnymi portami
  • Fail2Ban chroniący SSH
  • Automatyczne aktualizacje bezpieczeństwa włączone
  • Node.js zainstalowany przez NVM
  • PM2 uruchamiający twoją aplikację w trybie cluster
  • Skrypt startowy PM2 skonfigurowany (przetrwa restart)
  • Rotacja logów PM2 zainstalowana
  • Reverse proxy Nginx z odpowiednimi nagłówkami
  • SSL przez Let's Encrypt z automatycznym odnawianiem
  • Skrypt wdrożeniowy ze sprawdzeniami zdrowia
  • Swap file skonfigurowany (na zapas przy budowaniu)
  • Przetestowane: zrestartuj serwer i zweryfikuj, że wszystko wraca

Ten ostatni punkt to ten, który ludzie pomijają. Nie bądź tą osobą. Zrestartuj serwer, odczekaj 60 sekund i sprawdź, czy twoja aplikacja jest online. Jeśli nie jest, twoje skrypty startowe są źle skonfigurowane i dowiesz się o tym w najgorszym możliwym momencie.

Czy to jest "klasy enterprise"?#

Nie. I o to chodzi.

Ta konfiguracja niezawodnie serwuje tego bloga za mniej niż 10$/miesiąc. Wdraża się w 30 sekund jedną komendą. Rozumiem każdy jej element. Kiedy coś się psuje, wiem dokładnie, gdzie szukać.

Czy mógłbym użyć Docker? Jasne. Czy mógłbym użyć Kubernetes? Technicznie. Czy mógłbym skonfigurować pełny pipeline CI/CD ze środowiskami staging i wdrożeniami canary? Absolutnie.

Ale nauczyłem się, że najlepsza infrastruktura to ta, którą faktycznie rozumiesz, możesz debugować o 2 w nocy i nie kosztuje więcej, niż projekt zarabia. Dla osobistej strony, MVP SaaS lub małego startupu — to jest ta konfiguracja.

Najpierw dostarczaj. Skaluj, kiedy potrzebujesz. I zawsze, zawsze, testuj swój skrypt wdrożeniowy na świeżym serwerze.