Prosta architektura może być produkcyjna#

Dla jednej lub kilku aplikacji na VPS nie zawsze potrzebny jest Kubernetes. Hostowy Nginx, Docker Compose i kontrolowany proces wydania mogą zapewnić czytelną separację odpowiedzialności:

text
Internet
   |
Nginx :80/:443
   |
127.0.0.1:8001
   |
Kontener aplikacji :8000

Nginx odpowiada za TLS, przekierowania domen, nagłówki bezpieczeństwa, limity i logi dostępu. Kontener zawiera aplikację oraz jej zależności. Port aplikacji jest publikowany wyłącznie na 127.0.0.1, więc nie jest dostępny bezpośrednio z Internetu.

Bind do localhost ogranicza powierzchnię ataku#

W Compose:

yaml
services:
  website:
    build: .
    ports:
      - "127.0.0.1:8001:8000"

Różnica między 8001:8000 i 127.0.0.1:8001:8000 jest istotna. Pierwszy zapis zwykle publikuje port na wszystkich interfejsach hosta. Drugi ogranicza go do loopback. Nginx nadal może połączyć się z aplikacją, ale zewnętrzny klient nie ominie reverse proxy.

Hostowy firewall pozostaje potrzebny. Bind do localhost chroni konkretny port aplikacji, lecz nie zastępuje polityki dla SSH, Docker daemon, baz danych i pozostałych usług.

Reverse proxy powinno przekazać właściwy kontekst#

Minimalny blok:

nginx
location / {
    proxy_pass http://127.0.0.1:8001;
    proxy_http_version 1.1;
    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;
}

Aplikacja potrzebuje oryginalnego hosta i protokołu do generowania URL, logowania oraz reguł bezpieczeństwa. Jednocześnie powinna ufać forwarded headers tylko od kontrolowanego proxy. Jeżeli port aplikacji jest publiczny, klient może sam wstawić fałszywy X-Forwarded-For.

Dla WebSocket dodaj Upgrade i Connection. Dla uploadów ustaw świadomy client_max_body_size, a nie przypadkowo bardzo wysoki limit dla całej domeny.

TLS: najpierw działający HTTP challenge, potem docelowy vhost#

Pierwszy certyfikat nie istnieje jeszcze w /etc/letsencrypt/live/.... Jeżeli Nginx od razu odwołuje się do brakujących plików, nginx -t nie przejdzie. Bezpieczna sekwencja:

  1. uruchom tymczasowy vhost HTTP,
  2. udostępnij /.well-known/acme-challenge/,
  3. wykonaj certbot certonly --webroot,
  4. zainstaluj docelowy vhost HTTPS,
  5. wykonaj nginx -t,
  6. przeładuj Nginx.

Automatyczne odnowienie trzeba sprawdzić:

bash
certbot renew --dry-run

Sam istniejący certyfikat nie dowodzi, że timer, challenge i reload działają. Monitoruj termin ważności z zewnętrznego punktu.

Liveness, readiness i smoke test mają różne role#

Liveness powinien być prosty i stabilny. Odpowiada, czy proces potrafi obsłużyć podstawowe żądanie. Docker może używać go do oznaczenia kontenera jako healthy.

Readiness może sprawdzać wymagane pliki, konfigurację SMTP, połączenie z bazą albo migracje. Nie każde niepowodzenie readiness powinno powodować restart. Jeżeli zewnętrzny SMTP ma chwilową awarię, restart aplikacji może niczego nie naprawić.

Smoke test sprawdza publiczne ścieżki po uruchomieniu nowej wersji: stronę główną, podstrony, sitemap, API i numer wersji. To kontrola wydania, nie ciągły healthcheck.

Przykładowy healthcheck obrazu:

dockerfile
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
  CMD python -c "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/health', timeout=3)" || exit 1

Obraz powinien znać swoją wersję#

Wersja przekazana podczas builda powinna zostać zapisana jako label i zmienna środowiskowa:

dockerfile
ARG APP_VERSION=dev
LABEL org.opencontainers.image.version="${APP_VERSION}"
ENV APP_VERSION=${APP_VERSION}

Endpoint /version pozwala sprawdzić, co faktycznie działa. Nie nadpisuj wersji w Compose podczas startu, bo rollback starego obrazu może wtedy raportować numer nowej, nieudanej wersji.

Wydanie powinno mieć artefakt, sumę SHA256 i manifest. Dzięki temu wiadomo, jaki kod trafił na serwer, ile tras zawierał i czy paczka nie została zmieniona.

Aktualizacja bez zatrzymywania działającego kontenera podczas builda#

Bezpieczny skrypt:

  1. weryfikuje pliki i wersję,
  2. taguje aktualny obraz jako rollback,
  3. buduje nowy obraz,
  4. dopiero po sukcesie uruchamia nowy kontener,
  5. czeka na healthcheck,
  6. wykonuje smoke test,
  7. usuwa stare obrazy ponad limit.

Jeżeli build się nie powiedzie, działający kontener nie powinien zostać zmieniony. docker compose down przed buildem niepotrzebnie tworzy przerwę nawet wtedy, gdy kompilacja kończy się błędem.

Rollback musi używać zachowanego obrazu#

Przed zmianą odczytaj ID aktualnego obrazu i nadaj mu unikalny tag:

bash
STAMP="$(date +%Y%m%d-%H%M%S)"
docker tag current-image "app:rollback-${STAMP}"

Po nieudanym healthchecku lub smoke teście przywróć tag latest do poprzedniego obrazu i uruchom Compose bez builda. Następnie ponownie wykonaj test — rollback też może nie zadziałać, jeśli problem dotyczy bazy, hosta albo Nginx.

Zachowuj ograniczoną liczbę rollbacków. Nieograniczona retencja obrazów może wypełnić dysk i spowodować awarię podczas kolejnego wdrożenia.

Kontener read-only i minimalne uprawnienia#

Aplikacja statyczna lub API bez lokalnych zapisów może działać z read-only filesystem:

yaml
read_only: true
tmpfs:
  - /tmp:size=16m,mode=1777
cap_drop:
  - ALL
security_opt:
  - no-new-privileges:true

Obraz powinien uruchamiać proces jako użytkownik nie-root. Jeżeli aplikacja potrzebuje uploadów lub cache, przygotuj jawny wolumen z ograniczonymi uprawnieniami, zamiast wyłączać ochronę dla całego filesystemu.

Nagłówki, cache i CSP#

Nginx może centralnie ustawić:

  • HSTS po potwierdzeniu poprawnego HTTPS,
  • X-Content-Type-Options: nosniff,
  • Referrer-Policy,
  • Permissions-Policy,
  • Content-Security-Policy,
  • długi cache dla wersjonowanych assetów,
  • no-store dla API, health i version.

CSP wymaga utrzymania. Inline JSON-LD lub skrypt potrzebuje hasha albo nonce. Automatyczne generowanie hashy podczas wydania ogranicza ryzyko, że aktualizacja treści zablokuje własny kod.

Monitoring poza hostem#

Timer systemd na tym samym VPS może wykryć awarię aplikacji, ale nie wykryje utraty całego hosta lub sieci. Potrzebny jest zewnętrzny monitor co najmniej dla:

  • strony głównej,
  • /health, /ready i /version,
  • terminu TLS,
  • czasu odpowiedzi,
  • kontraktu formularza bez wysyłania wiadomości.

Lokalny monitor nadal jest użyteczny do SMTP probe, logów i szybkiego alertu przy błędzie części systemu.

Checklista wdrożenia#

  • Port kontenera jest zbindowany wyłącznie do localhost.
  • Nginx przekazuje Host i właściwe forwarded headers.
  • Certyfikat ma sprawdzony renew dry-run.
  • Obraz działa jako non-root i ma healthcheck.
  • /ready jest sprawdzany przed uznaniem wydania za gotowe.
  • /version odpowiada wersji labela obrazu.
  • Aktualizacja zachowuje poprzedni obraz.
  • Smoke test obejmuje publiczne trasy.
  • Rollback został przetestowany, a nie tylko opisany.
  • Monitoring działa również poza VPS-em.

Taki układ pozostaje prosty, ale nie jest improwizowany. Każda warstwa ma jasną odpowiedzialność, a proces wdrożenia posiada dowód sukcesu i przygotowaną drogę powrotu.