Założenia stacka i wymagania wstępne
Co znaczy „tani ale wydajny” w kontekście VPS
„Tani VPS” zwykle oznacza maszynę w granicach kilkunastu–kilkudziesięciu złotych miesięcznie, z 1–4 GB RAM, 1–2 vCPU i dyskiem SSD NVMe. Taki serwer wystarczy do utrzymania jednego lub kilku projektów PHP na Dockerze z Nginx i Redis, pod warunkiem sensownej konfiguracji. Kluczem jest świadome gospodarowanie pamięcią i I/O, a nie tylko wybór „mocniejszej” maszyny.
Wydajność w tym kontekście oznacza:
- czas odpowiedzi strony w ułamkach sekundy przy typowym ruchu (kilkaset–parę tysięcy odwiedzin dziennie),
- stabilność pod chwilowymi skokami ruchu (kampania, wpis na social media),
- brak „duszenia się” procesów PHP i bazy przez zbyt agresywną konfigurację.
Przy ograniczonym budżecie nie ma miejsca na marnowanie zasobów. Złe ustawienia PHP-FPM lub brak cache w Redis potrafią zabić nawet drogi serwer, a dobrze zestrojony stack Docker + Nginx + PHP-FPM + Redis działa zaskakująco sprawnie na 1–2 GB RAM.
Minimalne i zalecane parametry VPS pod Docker + Nginx + PHP-FPM + Redis
Dla pojedynczego projektu (np. WordPress, mały sklep, prosty SaaS) realne minimum to:
- 1 GB RAM – absolutne minimum, wymaga ostrej dyscypliny i ograniczania wszystkiego, co zbędne,
- 1 vCPU – wystarczy, dopóki nie ma ciężkich zadań w tle (generowanie raportów, przetwarzanie dużych plików),
- 20–30 GB SSD – spokojnie na OS, obrazy Dockera, dane bazy i logi dla małego projektu,
- transfer w przedziale 1–3 TB – przy typowym ruchu wystarczy, większość odwiedzin to niewielkie pliki HTML/CSS/JS.
Konfiguracja wygodna na produkcję dla kilku serwisów to:
- 2–4 GB RAM – komfortowe minimum dla kilku kontenerów PHP-FPM, bazy i Redis,
- 2 vCPU – sensowna równoległość przy zapytaniach do bazy i obsłudze ruchu HTTP,
- 40–80 GB SSD – jeśli trzymasz backupy lokalnie lub logi przez dłuższy czas, dysk potrafi się szybko zapełnić.
Przy Dockerze RAM „ucieka” w wielu miejscach: sam demon dockerd, obrazy, cache warstw, każdy kontener, system plików w pamięci. Dlatego na 1 GB RAM wymaga się minimalizmu – lekkie obrazy (np. Alpine), niewielka liczba procesów PHP, mocne ograniczenie usług pomocniczych i logów.
Dla jakich projektów taki stack ma sens
Stack VPS + Docker + Nginx + PHP-FPM + Redis jest idealny dla:
- małych i średnich sklepów internetowych na WordPress/WooCommerce lub prostych systemach sklepowych,
- blogów i portali kontentowych, którym zależy na dobrym TTFB i cache’owaniu,
- lekkich aplikacji SaaS w PHP (np. panele klienta, systemy rezerwacji),
- API w PHP (np. Slim, Lumen, Laravel w trybie API) do obsługi frontu SPA lub mobilnego,
- projektów klientów, gdzie jeden VPS służy do hostowania kilku mniejszych serwisów.
Jeśli ruch rośnie drastycznie lub aplikacja wykonuje ciężkie operacje CPU/IO, lepiej dodać zasoby albo rozważyć rozdzielenie bazy danych na osobny serwer. Dla większości małych biznesów jeden dobrze ustawiony VPS długo wystarcza, a zysk koszt/wydajność jest bardzo atrakcyjny.
Monolit kontra mikroserwisy na małym VPS
Mikroserwisy kuszą „nowoczesnością”, ale na tanim VPS często są przerostem formy nad treścią. Każdy mikroserwis to osobny kontener (lub kilka), własne zasoby, logika wdrożeń, monitoringu i backupów. Przy 2 GB RAM łatwo doprowadzić do sytuacji, gdzie 70% zasobów zjada sama infrastruktura.
Rozsądnym podejściem jest:
- monolit aplikacyjny w jednym kontenerze PHP-FPM (lub dwóch dla blue/green),
- oddzielny kontener Nginx jako reverse proxy,
- oddzielny kontener Redis – cache + sesje,
- oddzielny kontener bazy danych (MariaDB/PostgreSQL).
Taki podział to już osobne usługi, ale bez budowania pełnej architektury mikroserwisowej. Konfiguracja pozostaje prosta, a nadal zyskuje się modularność: można wymienić wersję PHP, podmienić Nginx, zrobić osobny backup bazy bez dotykania reszty.
Stack kontenerowy kontra klasyczny LAMP/LNMP
Klasyczny stack LAMP/LNMP instaluje Nginx/Apache, PHP i bazę bezpośrednio w systemie. Docker wprowadza warstwę pośrednią, ale ułatwia:
- utrzymanie kilku projektów z różnymi wersjami PHP i rozszerzeń,
- rollback do poprzedniej wersji obrazu bez kombinowania z pakietami,
- przenoszenie środowiska między serwerami bez przepisywania konfiguracji od zera,
- izolację aplikacji – jeden projekt nie rozwala konfiguracji drugiego.
Na bardzo małym VPS (1 GB RAM) klasyczny LNMP może być minimalnie lżejszy niż Docker. W praktyce jednak zyski z wygody wdrożeń, łatwego scale-out i powtarzalności konfiguracji przeważają. Przy poprawnym doborze obrazów kontenerowy stack nie musi być istotnie cięższy od gołej instalacji.
Wybór i przygotowanie VPS pod kontenery
Kryteria wyboru taniego VPS pod Docker
Przy wyborze VPS pod tani ale wydajny stack sprawdzane powinny być konkretne parametry, a nie tylko „RAM i dysk”:
- typ wirtualizacji – KVM, Xen lub podobne pełne wirtualizacje są bardziej elastyczne niż stare OVZ (OpenVZ); Docker na OVZ potrafi działać gorzej lub mieć ograniczenia,
- rodzaj dysku – SSD/NVMe jest obowiązkowy; HDD oznacza powolne zapytania bazy, długie odczyty logów i ogólnie ospały serwer,
- lokalizacja – im bliżej użytkowników, tym niższe opóźnienia; przy ruchu z Polski serwer w PL lub najbliższej Europie Środkowej będzie optymalny,
- transfer i polityka overage – przy przekroczeniu limitu niektórzy dostawcy drastycznie zwalniają łącze, co zabija wydajność,
- backup na poziomie dostawcy – snapshoty, backupy dzienne/tygodniowe; nawet mając własne kopie, backup providera bywa ostatnią deską ratunku.
Jeśli dostawca umożliwia łatwą migrację między planami, można zacząć od tańszej maszyny (np. 2 GB RAM) i w razie potrzeby przejść na 4 GB bez reinstalacji systemu. To często lepsza droga niż przepłacanie od pierwszego dnia.
Wybór systemu: Debian czy Ubuntu pod Docker
Na tani VPS pod Docker, Nginx i PHP-FPM najczęściej wybierany jest:
- Debian Stable – bardzo konserwatywny, przewidywalny system; minimalna ilość „magii”, spokojne aktualizacje,
- Ubuntu LTS – bazuje na Debianie, ale częściej aktualizowane pakiety, szerokie wsparcie tutoriali i narzędzi.
Obie dystrybucje mają świetną dokumentację Dockera i gotowe paczki od producenta. Różnice sprowadzają się głównie do wersji jądra i bibliotek, ale z punktu widzenia stacka Docker + Nginx + PHP-FPM + Redis wybór jednego z tych dwóch jest po prostu bezpieczny.
Początkowa konfiguracja systemu i zabezpieczenie SSH
Świeży VPS zwykle przychodzi z użytkownikiem root i logowaniem po haśle. To pierwsza rzecz do zmiany. Praktyczna sekwencja kroków:
- zalogowanie się jako
rooti utworzenie zwykłego użytkownika, np.deploy, - dodanie go do grupy
sudo(Ubuntu) lub konfiguracjasudow Debianie, - skonfigurowanie logowania po kluczu SSH,
- wyłączenie logowania root przez SSH,
- wyłączenie logowania hasłami, pozostawienie tylko kluczy.
Do tego dochodzi aktualizacja systemu: apt update i apt upgrade oraz instalacja podstawowych narzędzi (htop, vim/nano, curl, git). Taki fundament oszczędza sporo czasu w późniejszych etapach, szczególnie podczas debugowania problemów z obciążeniem lub siecią.
Podstawowy firewall dla Nginx i SSH
Na małym VPS nie ma sensu stawiać skomplikowanych zapór sprzętowych, ale podstawowy firewall z ufw lub czystym iptables mocno ogranicza powierzchnię ataku. Praktyczna polityka:
- zaakceptuj SSH (22/tcp, ewentualnie zmieniony port),
- zaakceptuj HTTP (80/tcp) i HTTPS (443/tcp),
- domyślnie odrzuć inne przychodzące połączenia,
- zezwól na wychodzące połączenia (aktualizacje, obrazy Dockera, DNS).
Jeśli Docker wystawia porty (np. baza danych), trzeba to robić tylko tam, gdzie faktycznie jest potrzeba. W większości przypadków baza i Redis są dostępne wyłącznie przez sieć Docker, bez publikowania portów na zewnątrz.
Ustawienia strefy czasowej, języka i hostname
Poprawnie ustawiona strefa czasowa (np. Europe/Warsaw) i hostname to drobiazgi, które oszczędzają nerwy. Logi Nginxa, PHP-FPM, systemu i Dockera będą miały spójne czasy. Analizując błędy czy piki ruchu, nie trzeba przeliczać godzin z UTC na lokalny czas w głowie.
Hostname ułatwia pracę z wieloma serwerami. Łatwiej odróżnić vps-prod-1 od vps-stage, niż „jakiegoś” localhost. W przypadku kilku projektów na jednym hostingu sensowny hostname porządkuje logi, monitoring i powiadomienia.
Instalacja Dockera i Docker Compose na serwerze
Dlaczego Docker nawet na małym VPS ma sens
Na tanim VPS wydaje się, że Docker wprowadza zbędny narzut. W praktyce daje:
- izolację – różne aplikacje nie nadpisują sobie konfiguracji PHP, pakietów systemowych ani zależności,
- łatwe deploymenty – zbudowany obraz można wgrać na produkcję i uruchomić bez „ręcznego” instalowania bibliotek,
- rollback – powrót do poprzedniej wersji polega na przełączeniu tagu obrazu lub cofnięciu
docker-compose.yml, - powtarzalność – to samo
docker-compose.ymldziała na devie, stage i produkcji, różnią się tylko pliki.env.
Wadą jest dodatkowy demon dockerd i pliki warstw, które zajmują miejsce na dysku. Przy rozsądnym użyciu komendy docker image prune i lekkich obrazach Alpine narzut jest jednak akceptowalny nawet na małej maszynie.
Instalacja Dockera z repozytoriów oficjalnych
Dystrybucje dostarczają własne paczki Dockera, ale są one często opóźnione. Lepszym wyborem na środowisko produkcyjne jest korzystanie z oficjalnego repozytorium Docker Inc. Zyskuje się:
- częstsze aktualizacje bezpieczeństwa,
- pełne wsparcie najnowszych funkcji,
- spójność między dev i prod, jeśli deweloperzy też używają oficjalnego Dockera.
Instalacja na Debian/Ubuntu sprowadza się do:
- dodania klucza GPG i repozytorium,
- instalacji
docker-ce,docker-ce-cliorazcontainerd.io, - dodania użytkownika (np.
deploy) do grupydocker.
Po tym docker ps powinno działać bez użycia sudo (po ponownym zalogowaniu się użytkownika). To ważne, bo większość automatyzacji będzie wywoływać Dockera z konta nie-root.
Instalacja Docker Compose: plugin czy binarka
Obecnie Docker promuje Compose jako plugin (docker compose), a nie oddzielną binarkę docker-compose. Różnice:
- plugin jest częścią oficjalnych paczek i integruje się z
docker(jedno polecenie, ta sama konfiguracja CLI), - klasyczna binarka
docker-composepozwala zachować nawyki, ale bywa wolniej aktualizowana na serwerze.
Na nowym VPS lepiej od razu przejść na docker compose. W większości tutoriali polecenia różnią się tylko myślnikiem, a w skryptach można to łatwo zmienić.
Jeśli z jakiegoś powodu potrzebna jest stara binarka (np. starsze skrypty deployujące używają docker-compose), można mieć oba narzędzia równolegle. W praktyce wygodnie jest wprowadzić alias w powłoce, np. alias docker-compose='docker compose', dzięki czemu nowe polecenia korzystają z pluginu, a stare dokumentacje i nawyki nie przeszkadzają w codziennej pracy.
Przy pierwszym uruchomieniu warto od razu sprawdzić działanie całego stosu: docker run hello-world weryfikuje poprawną instalację silnika, a proste docker compose up z minimalnym plikiem YAML ujawnia problemy z uprawnieniami, siecią lub pluginem Compose. Lepiej wyłapać takie drobiazgi na pustym serwerze niż podczas nerwowego wdrożenia krytycznej aplikacji.
Na małych VPS-ach dużą różnicę robi też porządkowanie zasobów Dockera. Po kilku eksperymentach z obrazami PHP, Nginxa i Redis wystarczy kilka komend porządkowych (docker image prune, docker container prune, okresowo także docker volume prune), aby odzyskać gigabajty przestrzeni. Jeśli aplikacja loguje dużo do STDOUT, dobrze jest skonfigurować rotację logów Dockera albo przełączyć kontenery na własny katalog logów z rotacją po stronie hosta.

Projekt architektury stacka: Nginx, PHP-FPM, Redis i baza danych
Podział usług na kontenery a zasoby małego VPS
Na małym VPS-ie kluczowe jest, żeby każdy kontener miał jasną odpowiedzialność, ale jednocześnie nie mnożyć bytów bez potrzeby. Typowy, rozsądny podział:
- nginx – serwowanie statycznych plików, reverse proxy do PHP-FPM, terminacja TLS,
- php-fpm – proces interpretujący PHP, bezpośrednio nie wystawiony na Internet,
- redis – cache sesji / obiektów / zapytań, dostępny tylko z sieci wewnętrznej Dockera,
- baza danych – najczęściej MariaDB/MySQL lub PostgreSQL; na bardzo małym VPS bywa sens umieszczenia bazy na oddzielnym, zarządzanym serwisie.
Jeśli VPS ma np. 2 GB RAM, zbyt wiele kontenerów oznacza trudniejszą kontrolę pamięci. W takim scenariuszu lepiej mieć:
- jeden wspólny php-fpm dla kilku prostych aplikacji (ale z rozdzielonymi userami i pulami procesów),
- jedną instancję Redis z logicznym podziałem na bazy (DB 0, 1, 2…),
- bazę danych skonfigurowaną oszczędnie (mały
innodb_buffer_pool_sizelub odpowiednik dla PostgreSQL).
Sieci Dockera: oddzielenie frontu od wnętrza
Dobrą praktyką jest stworzenie co najmniej jednej sieci typu bridge dla usług aplikacyjnych. Przykład:
frontend– sieć, do której podłączony jestnginxoraz kontenery aplikacji (PHP-FPM),backend– sieć, w której siedzi baza danych i Redis, dostępna tylko z aplikacji, nie z Nginxa.
W prostych projektach można pozostać przy jednej sieci, ale sensowny jest przynajmniej podział logiczny:
- Nginx widzi tylko kontenery, do których ma routować (PHP),
- PHP widzi bazę i Redis, ale te nie wystawiają portów na hosta.
Dzięki temu porty typu 6379 (Redis) czy 3306 (MySQL) są dostępne wyłącznie wewnątrz sieci Dockera, a firewall hosta ma mniej do roboty.
Utrwalenie danych – wolumeny dla bazy i konfiguracji
Na produkcji kontenery muszą być w pełni wymienne. Wszystko, co ma przetrwać restart, ląduje w wolumenach:
- baza danych – główny katalog danych (np.
/var/lib/mysql,/var/lib/postgresql/data) jako wolumen nazwany lub bind mount, - konfiguracja Nginxa – katalog z
nginx.confi wirtualnymi hostami montowany z hosta, żeby można było szybko coś poprawić bez przebudowy obrazu, - logi – przy większym ruchu lepiej trzymać je na hostcie i mieć rotację poprzez
logrotate, niż liczyć na log drivera Dockera.
Redis w typowej roli cache można utrzymywać w pełni w pamięci i nie montować żadnego wolumenu. Jeśli przechowuje np. sesje użytkowników, często wystarczy akceptowalny scenariusz: restart = utrata sesji.
Zarządzanie zmiennymi środowiskowymi i sekretami
W prostym stacku zmienne środowiskowe najczęściej lądują w pliku .env obok docker-compose.yml. Elementy jak:
- hasło do bazy danych,
- klucze aplikacyjne (np.
APP_KEY), - dane do Redis (host, port, opcjonalne hasło),
- parametry PHP (limit pamięci, max upload).
Na pojedynczym VPS-ie pełne użycie Docker Secrets bywa przerostem formy, ale trzymanie pliku .env poza repozytorium GIT (np. .gitignore) i z ograniczonymi uprawnieniami (chmod 600) to absolutne minimum.
Przygotowanie docker-compose.yml dla prostego projektu PHP
Struktura katalogów na serwerze
Porządek w katalogach ułatwia późniejsze migracje. Sprawdza się prosty układ:
/srv/www/
myapp/
docker-compose.yml
.env
nginx/
default.conf
src/
index.php
...
Katalog src można aktualizować przez GIT, rsync czy CI/CD. Konfigurację Nginxa i docker-compose.yml trzyma się obok, co od razu pokazuje, które pliki są infrastrukturą, a które aplikacją.
Minimalny docker-compose.yml z Nginx, PHP-FPM i Redis
Przykładowy plik dla prostej aplikacji PHP (np. mały framework lub własne MVC):
version: "3.9"
services:
nginx:
image: nginx:stable-alpine
container_name: myapp_nginx
ports:
- "80:80"
volumes:
- ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
- ./src:/var/www/html:ro
depends_on:
- php
networks:
- app_net
php:
image: php:8.2-fpm-alpine
container_name: myapp_php
volumes:
- ./src:/var/www/html
environment:
PHP_MEMORY_LIMIT: 256M
PHP_UPLOAD_MAX_FILESIZE: 20M
PHP_MAX_EXECUTION_TIME: 30
networks:
- app_net
- db_net
redis:
image: redis:7-alpine
container_name: myapp_redis
command: ["redis-server", "--appendonly", "no"]
networks:
- db_net
db:
image: mariadb:10.6
container_name: myapp_db
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
MYSQL_DATABASE: ${MYSQL_DATABASE}
MYSQL_USER: ${MYSQL_USER}
MYSQL_PASSWORD: ${MYSQL_PASSWORD}
volumes:
- db_data:/var/lib/mysql
networks:
- db_net
networks:
app_net:
db_net:
volumes:
db_data:
Przy takiej konfiguracji jedynym publicznie dostępnym serwisem jest Nginx (port 80 hosta mapowany na 80 w kontenerze). PHP, Redis i baza komunikują się po sieci Dockera.
Dobór obrazów: Alpine czy pełne dystrybucje
Na tanim VPS różnicę robi każdy megabajt pamięci i dysku. Obrazy Alpine mają:
- znacznie mniejszą wielkość (szybsze pobieranie, mniej miejsca na dysku),
- mniej domyślnych narzędzi – trzeba doinstalować konkretne pakiety.
Dla typowej aplikacji PHP:
php:8.2-fpm-alpine– dobra baza, do której można doinstalować rozszerzenia,nginx:stable-alpine– w zupełności wystarcza do reverse proxy i statyków.
Jeśli aplikacja korzysta z bardziej egzotycznych rozszerzeń, które trudno skompilować w Alpine, można rozważyć obrazy oparte o Debian/Ubuntu (php:8.2-fpm). Ceną będzie większy rozmiar i potencjalnie wyższe zużycie RAM.
Budowanie własnego obrazu PHP-FPM z rozszerzeniami
Większość realnych projektów PHP wymaga czegoś więcej niż goły PHP: rozszerzenia pdo_mysql, intl, gd, czasem opcache. W takim przypadku tworzy się prosty Dockerfile:
FROM php:8.2-fpm-alpine
RUN apk add --no-cache
icu-dev
libpng-dev
libjpeg-turbo-dev
libwebp-dev
freetype-dev
oniguruma-dev
&& docker-php-ext-configure gd
--with-freetype
--with-jpeg
--with-webp
&& docker-php-ext-install
pdo_mysql
intl
gd
opcache
&& rm -rf /var/cache/apk/*
WORKDIR /var/www/html
W docker-compose.yml sekcję php zmienia się na:
php:
build:
context: .
dockerfile: Dockerfile
container_name: myapp_php
volumes:
- ./src:/var/www/html
networks:
- app_net
- db_net
Przy pierwszym docker compose up --build -d obraz zostanie zbudowany i zapisany lokalnie. Późniejsze zmiany w kodzie nie wymagają przebudowy, o ile nie zmienia się lista rozszerzeń PHP ani zależności systemowych.
Plik .env – separacja konfiguracji od kodu
Plik .env może wyglądać następująco:
MYSQL_ROOT_PASSWORD=super_tajne_root
MYSQL_DATABASE=myapp
MYSQL_USER=myapp_user
MYSQL_PASSWORD=super_tajne_haslo
APP_ENV=prod
APP_DEBUG=0
APP_URL=https://example.com
REDIS_HOST=redis
REDIS_PORT=6379
W kodzie aplikacji zmienne odczytuje się z $_ENV lub poprzez funkcje frameworka (np. env() w Laravelu). Dzięki temu ten sam kod działa na lokalnym devie i na produkcji, a konfiguracja zmienia się tylko przez .env.
Podstawowe limity zasobów per kontener
Na ciasnej maszynie dobrym ruchem jest nałożenie prostych limitów w docker-compose.yml:
services:
php:
deploy:
resources:
limits:
memory: 512M
Sekcja deploy jest w pełni respektowana przez Swarma, ale część ustawień (zwłaszcza limitów pamięci) działa również na pojedynczym hoście, o ile kernel ma włączone cgroups. Jeśli kontener zacznie przekraczać ustalony limit, szybciej wyjdą na jaw problemy z niewydajnym kodem lub zbyt dużymi procesami roboczymi.
Konfiguracja Nginx jako reverse proxy dla PHP-FPM w Dockerze
Podstawowy vhost Nginxa dla PHP-FPM
Kluczowy element to plik w stylu nginx/default.conf, który Nginx w kontenerze zaczyta przy starcie. Przykładowa konfiguracja:
server {
listen 80;
server_name _;
root /var/www/html/public;
index index.php index.html;
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log warn;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ .php$ {
include fastcgi_params;
fastcgi_pass myapp_php:9000;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param SCRIPT_NAME $fastcgi_script_name;
}
location ~* .(jpg|jpeg|png|gif|ico|css|js|svg|woff2?)$ {
expires 7d;
add_header Cache-Control "public, max-age=604800, immutable";
try_files $uri =404;
}
client_max_body_size 20m;
}
Najważniejsza linia to fastcgi_pass myapp_php:9000;. myapp_php to nazwa usługi z docker-compose.yml, jaką widzi Nginx wewnątrz sieci Dockera, a port 9000 to domyślny port FPM.
Spójność ścieżek: root i SCRIPT_FILENAME
Nginx i PHP-FPM muszą widzieć ten sam kod pod tymi samymi ścieżkami. Jeśli w docker-compose.yml montujemy:
nginx:
volumes:
- ./src:/var/www/html:ro
php:
volumes:
- ./src:/var/www/html
to w konfiguracji Nginxa root musi wskazywać na ten sam katalog (/var/www/html) lub jego podkatalog (np. /var/www/html/public). W przeciwnym razie SCRIPT_FILENAME będzie wskazywał na inną ścieżkę niż to, co widzi PHP-FPM, co kończy się błędami 404 lub 502.
Obsługa błędów i tryb „maintenance”
Nginx może przechwytywać błędy aplikacji i wyświetlać własne strony:
server {
# ...
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /var/www/html/public;
}
}
Prosty tryb maintenance można zrealizować przez plik maintenance.html oraz regułę:
if (-f /var/www/html/public/maintenance.html) {
return 503;
}
error_page 503 @maintenance;
location @maintenance {
rewrite ^(.*)$ /maintenance.html break;
}
Wystarczy wgrać lub usunąć plik maintenance.html, aby przełączyć aplikację w tryb serwisowy, bez restartu kontenerów.
Podstawowe zabezpieczenia i nagłówki
Prosta konfiguracja Nginxa potrafi znacząco podnieść poziom bezpieczeństwa:
server {
# ...
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
# Ukrycie wersji Nginxa
server_tokens off;
# Blokada dostępu do plików .git, .env itd.
location ~ /.(git|env|ht) {
deny all;
}
}
Część nagłówków można również dodać na poziomie aplikacji PHP lub frameworka, ale jeśli jest ich kilka i dotyczą całej domeny, wygodniej zarządzać nimi centralnie w Nginxie.
Integracja z TLS (LetsEncrypt) na pojedynczym VPS
Na tanim VPS certyfikat TLS najczęściej jest realizowany przez Certbota w kontenerze lub bezpośrednio na hoście. Prosty i skuteczny sposób:
na hoście uruchomionym przed Nginxem w Dockerze. W takim scenariuszu port 443 na serwerze przypisany jest do hostowego Nginxa lub innego terminatora TLS, a kontener z Nginxem aplikacyjnym nasłuchuje tylko na porcie 80 wewnątrz Dockera i komunikuje się z proxy po HTTP. Przy małym VPS wygodniejsze bywa jednak zapięcie TLS bezpośrednio w kontenerze z Nginxem.
Minimalny wariant zakłada mapowanie katalogu z certyfikatami do kontenera oraz dodanie bloku server dla HTTPS. W docker-compose.yml można dopisać:
nginx:
# ...
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/conf.d:/etc/nginx/conf.d:ro
- ./certs:/etc/letsencrypt:ro
Następnie w konfiguracji Nginxa tworzy się serwer z TLS i przekierowaniem z HTTP na HTTPS:
server {
listen 80;
server_name example.com www.example.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name example.com www.example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
include /etc/nginx/snippets/ssl-params.conf;
root /var/www/html/public;
index index.php index.html;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ .php$ {
include fastcgi_params;
fastcgi_pass myapp_php:9000;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
}
Certbot może działać na hoście i jedynie zapisuje pliki w katalogu ./certs, który jest współdzielony z kontenerem Nginxa. Odświeżanie certyfikatów (cron lub timer systemd) nie wymaga restartu kontenerów – Nginx pobierze nowe pliki z tego samego mountu, a lekkie nginx -s reload można zainicjować komendą docker exec myapp_nginx nginx -s reload.
Na małych VPS dobrze sprawdza się też prosty układ: Nginx w kontenerze, Certbot na hoście, a do weryfikacji HTTP-01 krótkotrwale przekierowuje się port 80 hosta na katalog /.well-known/acme-challenge w kontenerze. Nie jest to tak eleganckie jak dedykowane obrazy typu nginx-proxy + letsencrypt-nginx-proxy-companion, ale przy jednym projekcie i jednym domenowym certyfikacie utrzymanie całości pozostaje zrozumiałe i tanie zasobowo.

Podłączenie Redis i bazy danych do aplikacji PHP
Konfiguracja połączenia z Redisem w PHP
Przy uruchomionym serwisie redis w docker-compose.yml aplikacja PHP widzi go pod nazwą hosta redis na porcie 6379. Prosty przykład z użyciem rozszerzenia phpredis:
<?php
$redisHost = getenv('REDIS_HOST') ?: 'redis';
$redisPort = getenv('REDIS_PORT') ?: 6379;
$redis = new Redis();
$redis->connect($redisHost, (int) $redisPort, 1.5);
$redis->set('foo', 'bar', 60); // klucz foo na 60 sekund
echo $redis->get('foo');
W przypadku frameworków (Laravel, Symfony) konfiguracja zwykle sprowadza się do podania hosta i portu w pliku konfiguracyjnym lub w .env. Przykład dla Laravela:
REDIS_HOST=redis
REDIS_PASSWORD=null
REDIS_PORT=6379
W config/database.php (Laravel) sekcja Redis powinna pobierać te wartości z env(), co pozwoli łatwo przerzucić się na zewnętrzny, zarządzany Redis bez modyfikowania kodu.
Konfiguracja PDO MySQL w kontenerze PHP
Serwis bazy danych w docker-compose.yml może nazywać się np. db. PHP łączy się wtedy pod hostem db:
<?php
$dsn = sprintf(
'mysql:host=%s;dbname=%s;charset=utf8mb4',
getenv('MYSQL_HOST') ?: 'db',
getenv('MYSQL_DATABASE') ?: 'myapp'
);
$user = getenv('MYSQL_USER') ?: 'myapp_user';
$pass = getenv('MYSQL_PASSWORD') ?: 'super_tajne_haslo';
$pdo = new PDO($dsn, $user, $pass, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
Warto zdefiniować w .env także MYSQL_HOST=db, żeby ten sam kod bez zmian działał przy przenosinach na inny serwer lub przy użyciu zewnętrznej bazy (np. usŁuga zarządzana).
Typowe problemy z łącznością między kontenerami
Jeśli aplikacja nie może połączyć się z Redisem lub bazą, zwykle winne są:
- użycie
localhostzamiast nazwy serwisu Docker (np.db,redis), - brak wspólnej sieci w
docker-compose.yml, - zły port (próba użycia portu hosta zamiast portu kontenera).
Dobrą praktyką jest logowanie adresu hosta i portu przy starcie aplikacji (np. w logach debugowych), co pozwala szybciej wychwycić błędne wartości ze zmiennych środowiskowych.
Cache aplikacyjny i PHP-FPM – praktyczne ustawienia
Prosty cache HTTP w Nginxie
Na małym VPS opłaca się wykorzystać Nginxa do cache’owania odpowiedzi statycznych lub pół-dynamicznych. Minimalny przykład dla prostych endpointów GET:
proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=mycache:10m
inactive=60m use_temp_path=off;
server {
# ...
location /api/public/ {
proxy_cache mycache;
proxy_cache_valid 200 1m;
add_header X-Cache-Status $upstream_cache_status;
try_files $uri $uri/ /index.php?$query_string;
}
}
W scenariuszu PHP-FPM zamiast proxy_pass używa się FastCGI, ale sam mechanizm (cache na poziomie Nginxa) pozostaje podobny – to już jednak wyraźnie bardziej zaawansowana konfiguracja i przy prostym projekcie często wystarcza cache aplikacyjny oraz Redis.
Opcache w PHP – podstawa wydajności
Rozszerzenie opcache jest niezbędne, jeśli aplikacja ma reagować szybko przy większym ruchu. W obrazie PHP-FPM wystarczy dodać podstawową konfigurację, np. w pliku php/conf.d/opcache.ini:
zend_extension=opcache.so
opcache.enable=1
opcache.enable_cli=0
opcache.memory_consumption=128
opcache.interned_strings_buffer=16
opcache.max_accelerated_files=10000
opcache.validate_timestamps=1
opcache.revalidate_freq=2
opcache.fast_shutdown=1
Przy małych projektach 64–128 MB pamięci na Opcache zwykle pokrywa wszystkie skrypty. Gdy aplikacja rośnie, pierwszym sygnałem problemu jest częste odrzucanie starych wpisów z Opcache, co widać w metrykach (opcache_get_status()).
Dopasowanie liczby procesów PHP-FPM do RAM
Domyślna konfiguracja FPM często tworzy zbyt wiele procesów w stosunku do pamięci VPS. Trzeba policzyć orientacyjnie: jeśli pojedynczy proces PHP zużywa np. 60–80 MB, to na maszynie z 2 GB RAM trudno utrzymać kilkanaście procesów, bazę i Redis bez ryzyka swapa.
Konfiguracja dla poola www (np. w /usr/local/etc/php-fpm.d/www.conf):
pm = dynamic
pm.max_children = 8
pm.start_servers = 2
pm.min_spare_servers = 2
pm.max_spare_servers = 4
pm.max_requests = 500
Dla ciasnego VPS często lepiej zejść z pm.max_children na 4–6, ale ustawić sensowną wartość pm.max_requests, żeby procesy były regularnie recyklingowane (redukcja wycieków pamięci w aplikacji).
Tryb developerski vs produkcyjny w tym samym stacku
Osobne pliki docker-compose dla dev i prod
Praktyczne podejście to utrzymanie dwóch plików:
docker-compose.yml– podstawowy stack (prod),docker-compose.override.yml– nadpisania dla dev.
Przykład pliku docker-compose.override.yml do pracy lokalnej:
version: "3.9"
services:
php:
environment:
APP_ENV: dev
APP_DEBUG: 1
volumes:
- ./src:/var/www/html
- ./php-dev.ini:/usr/local/etc/php/conf.d/zz-dev.ini
nginx:
ports:
- "8080:80"
Lokalnie docker compose up wczyta oba pliki, a na serwerze produkcyjnym wystarczy użyć tylko głównego docker-compose.yml lub przygotować osobny plik docker-compose.prod.yml z innymi limitami i bez debugów.
Automatyczne przełączanie na debug w oparciu o APP_ENV
W kodzie warto oprzeć logikę debugowania na zmiennej APP_ENV. Przykładowy bootstrap:
<?php
$env = getenv('APP_ENV') ?: 'prod';
if ($env === 'dev') {
ini_set('display_errors', '1');
ini_set('display_startup_errors', '1');
error_reporting(E_ALL);
} else {
ini_set('display_errors', '0');
error_reporting(E_ALL & ~E_DEPRECATED & ~E_STRICT);
}
Dzięki temu ten sam obraz Dockera działa i na devie, i na produkcji, a zmiana trybu sprowadza się do podmiany zmiennych środowiskowych.

Logowanie i monitoring na małym VPS
Gdzie trafiają logi kontenerów
Domyślnie Docker trzyma logi w plikach JSON na hoście. Przy długim działaniu i większym ruchu mogą one rosnąć bez kontroli. Należy włączyć rotację logów per kontener w docker-compose.yml:
services:
nginx:
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "5"
php:
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
Dostęp do logów uzyskuje się przez docker compose logs -f nginx lub bezpośrednio z hosta (ścieżka zależy od distro i konfiguracji Dockera).
Przekierowanie logów PHP-FPM do stdout
Żeby nie szukać logów w różnych katalogach w kontenerze, opłaca się kierować błędy PHP na stderr/stdout. W pliku php.ini:
log_errors = On
error_log = /proc/self/fd/2
Dzięki temu wszystkie komunikaty lądują w logach Dockera, które da się oglądać za pomocą jednej komendy. Dla FPM można też zmodyfikować www.conf:
access.log = /proc/self/fd/2
catch_workers_output = yes
Prosty healthcheck dla serwisów
Na dłuższą metę przydaje się mechanizm healthchecków. Docker może w prosty sposób sprawdzać, czy serwis odpowiada. Przykład dla Nginxa:
services:
nginx:
# ...
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost/health"]
interval: 30s
timeout: 3s
retries: 3
Aplikacja powinna udostępniać lekki endpoint /health zwracający np. status 200 i krótką informację. Jeśli healthcheck zacznie regularnie padać, w logach Dockera od razu widać, że coś jest nie tak – nawet bez rozbudowanego monitoringu.
Backupy i aktualizacje przy stacku na Dockerze
Backupy bazy danych z użyciem wolumenu
Najprostsza metoda: okresowy mysqldump wykonywany w osobnym, tymczasowym kontenerze. Przykład skryptu backupowego na hoście:
#!/bin/bash
set -e
BACKUP_DIR=/var/backups/myapp
DATE=$(date +%Y%m%d-%H%M)
mkdir -p "$BACKUP_DIR"
docker compose exec -T db
mysqldump -u"$MYSQL_USER" -p"$MYSQL_PASSWORD" "$MYSQL_DATABASE"
> "$BACKUP_DIR/db-$DATE.sql"
Taki skrypt można podpiąć pod crona. Przy większej bazie przydaje się kompresja (gzip) i rotacja starych dumpów.
Aktualizacje obrazów bez przestojów nieproporcjonalnych do skali
Na jednym tanim VPS nie da się zrobić pełnego blue/green, ale i tak można ograniczyć przestój. Podstawowy scenariusz:
- pobrać nowe obrazy:
docker compose pull, - zrobić krótki backup bazy i plików,
- przeładować stack:
docker compose up -d, - sprawdzić logi i endpoint
/health.
Dla pojedynczej aplikacji zwykle oznacza to kilkanaście sekund przerwy. Jeśli priorytetem jest ciągły dostęp, można najpierw podnieść nowe kontenery (np. myapp_php_v2), a następnie przepiąć ruch w konfiguracji Nginxa na nową usługę i dopiero po testach wyłączyć stary wariant.
Uproszczony workflow wdrożeniowy z Git i Dockerem
Pull z repozytorium i rebuild obrazów
Prosty, ale skuteczny workflow na małym VPS opiera się na jednym repozytorium Git z katalogami:
src/– kod aplikacji,nginx/– konfiguracja,docker-compose.yml,Dockerfile,.env– pliki stacka.
Sekwencja wdrożenia:
ssh vps
cd /var/www/myapp
git pull origin main
docker compose pull # aktualizacja bazowych obrazów, jeśli trzeba
docker compose up --build -d # przebudowa obrazu php + restart usług
docker compose ps # szybka weryfikacja statusu
W przypadku drobnych zmian w kodzie PHP (bez zmiany rozszerzeń i zależności systemowych) przejście na obraz budowany lokalnie nie jest konieczne – wystarczy zaciągnąć zmiany i zrobić docker compose up -d, jeśli kod jest montowany jako wolumen.
Bezpieczne przechowywanie .env na serwerze
Plik .env nie powinien trafiać do repozytorium. Na serwerze najlepiej trzymać go w katalogu projektu z uprawnieniami:
chmod 600 .env
chown root:root .env
Konfigurację można dodatkowo eksportować do zmiennych środowiskowych przy starcie usług (np. w docker-compose.yml używając env_file: .env). Przy zmianie haseł lub tokenów wystarczy zaktualizować .env i wykonać docker compose up -d, bez dotykania kodu aplikacji.
Najczęściej zadawane pytania (FAQ)
Jaki jest minimalny sensowny VPS pod Docker + Nginx + PHP-FPM + Redis?
Absolutne minimum dla pojedynczej aplikacji PHP (np. mały WordPress) to 1 GB RAM, 1 vCPU i około 20–30 GB SSD. Przy takich zasobach trzeba bardzo pilnować liczby kontenerów, procesów PHP-FPM, logów i wszelkich usług dodatkowych.
Jeśli planujesz kilka serwisów lub sklep z większym ruchem, praktycznym minimum jest 2 GB RAM, 2 vCPU i 40+ GB SSD na system, obrazy Dockera, bazę, logi oraz ewentualne backupy lokalne.
Czy Docker na tanim VPS ma sens, czy lepiej postawić klasyczny LAMP/LNMP?
Na bardzo małej maszynie (1 GB RAM) klasyczny LNMP bywa minimalnie lżejszy, bo nie ma narzutu na demona dockerd i warstwy obrazów. Jeśli jednak chcesz utrzymywać kilka projektów, mieć różne wersje PHP i wygodne wdrożenia, Docker daje wyraźną przewagę organizacyjną.
Przy 2 GB RAM i więcej dobrze dobrane obrazy (np. oparte na Alpine) powodują, że różnica w zużyciu zasobów przestaje być krytyczna, a zyski z izolacji, rollbacków i łatwej migracji między serwerami są bardzo odczuwalne.
Czy na tanim VPS lepiej postawić monolit czy mikroserwisy w Dockerze?
Na tanim VPS (1–2 GB RAM) lepszym wyborem jest monolit w jednym kontenerze PHP-FPM, a obok osobne kontenery dla Nginx, Redis i bazy danych. Taki układ jest prosty, czytelny i nie „przepala” zasobów na rozbudowaną infrastrukturę.
Rozbijanie aplikacji na wiele mikroserwisów generuje dużą liczbę kontenerów, dodatkowe logi, konfigurację monitoringu i backupów. Na małej maszynie kończy się to często tym, że większość RAM zużywa sama orkiestracja, a nie kod aplikacji.
Jaki VPS wybrać pod tani, ale wydajny stack z Redis i PHP-FPM?
Poza RAM i CPU trzeba patrzeć na kilka kluczowych elementów: typ wirtualizacji (KVM/Xen zamiast starego OpenVZ), dysk SSD/NVMe, lokalizację serwera blisko użytkowników oraz politykę transferu (co się dzieje po przekroczeniu limitu).
Dobry dostawca oferuje snapshoty lub backupy na poziomie infrastruktury i umożliwia łatwą migrację między planami. Pozwala to wystartować z mniejszą maszyną, a przy rosnącym ruchu podnieść parametry bez reinstalacji systemu.
Debian czy Ubuntu pod Docker, Nginx i PHP-FPM na VPS?
Najczęściej wybierany jest Debian Stable lub Ubuntu LTS. Debian jest bardziej konserwatywny i przewidywalny, co sprzyja spokojnym aktualizacjom. Ubuntu LTS ma częściej aktualizowane pakiety i ogromną bazę gotowych tutoriali oraz narzędzi.
Z punktu widzenia Dockera, Nginx, PHP-FPM i Redis oba systemy są bezpiecznym wyborem: mają oficjalne repozytoria Dockera, dobrą dokumentację i przewidywalne cykle wsparcia. Decyzja zwykle sprowadza się do preferencji administratora.
Jak zabezpieczyć tani VPS z Dockerem (Nginx, Redis, PHP-FPM) na start?
Podstawowy zestaw kroków to: utworzenie użytkownika nie-root (np. deploy), konfiguracja sudo, przejście na logowanie wyłącznie kluczem SSH, wyłączenie logowania root oraz logowania hasłami. Po tym od razu wykonuje się aktualizacje systemu i instaluje podstawowe narzędzia diagnostyczne.
Następny krok to prosty firewall (ufw lub iptables): otworzenie tylko portów SSH, HTTP i HTTPS, a resztę ruchu przychodzącego zablokować. Porty baz danych i innych usług najlepiej zostawiać niewystawione „na świat” i używać sieci Dockera lub tuneli SSH.
Do jakich projektów wystarczy jeden tani VPS z Docker + Nginx + PHP-FPM + Redis?
Taki stack dobrze obsłuży małe i średnie sklepy internetowe (np. WooCommerce), blogi i portale z naciskiem na szybkie TTFB, lekkie aplikacje SaaS w PHP oraz API dla frontów SPA czy aplikacji mobilnych. Jeden VPS może z powodzeniem obsłużyć kilka takich serwisów, jeśli konfiguracja jest oszczędna.
Jeśli pojawiają się ciężkie zadania w tle (importy, raporty, przetwarzanie plików) albo ruch rośnie mocno, pierwszym krokiem jest zwykle powiększenie parametrów VPS. W kolejnym etapie można rozdzielić bazę danych na osobny serwer, zostawiając Nginx, PHP-FPM i Redis na pierwotnej maszynie.






