Konfiguracja Apache VirtualHost na Ubuntu i Debian — pełny przewodnik krok po kroku
Postawienie VirtualHosta w Apache wygląda na zadanie banalne, dopóki nie złapiesz trzeciego błędu 403, certyfikat odmawia odnowienia, albo PHP-FPM po cichu wraca do mod_php i nie wiesz, dlaczego. Ten przewodnik prowadzi przez produkcyjną konfigurację Apache VirtualHost na Ubuntu i Debian — z SSL od Let's Encrypt, PHP-FPM, osobnymi logami i zestawem dyrektyw bezpieczeństwa, które powinny być w każdej konfiguracji, ale rzadko są. Na końcu masz plik .conf gotowy do wrzucenia do /etc/apache2/sites-available/, włączenia przez a2ensite i pewność, że nie ugryzie Cię za trzy miesiące.
Tekst pisany jest dla administratora, który zna terminal, ale nie chce uczyć się na pamięć każdej dyrektywy Apache. Jeśli to Ty — reszta artykułu jest brakującą instrukcją.
Generator VirtualHost — co robi i komu się przyda
Generator konfiguracji Apache VirtualHost na vps-web.com tworzy kompletny plik .conf z krótkiego formularza: domena, alias, SSL, wersja PHP (od 5.6 do 8.5 — łącznie z apache-mod dla rzadkich przypadków, gdy jest jeszcze potrzebny), osobne logi, wzmocnienie bezpieczeństwa i buforowanie statycznych plików. Output wkleja się prosto do /etc/apache2/sites-available/ pod nazwą zgodną z ServerName, gotowy do a2ensite.
Powstał, ponieważ pisanie tego samego szablonu po raz dziesiąty z głowy to strata wieczoru, a kopiowanie ze starych konfiguracji ciągnie za sobą przestarzałe dyrektywy w stylu Order allow,deny (wycofane od Apache 2.4) czy handlery mod_php, które nie działają z HTTP/2. Generator produkuje nowoczesną składnię — Require all granted, PHP-FPM przez proxy_fcgi, Protocols h2 http/1.1 — i pomija to, co Twoja dystrybucja już zapewnia w /etc/apache2/conf-enabled/.
Narzędzie celuje w pojedyncze serwery: VPS-y, domowe homelaby, małe serwery firmowe. Jeśli prowadzisz klaster zarządzany przez Ansible albo Puppeta, prawdopodobnie masz już własne szablony. Jeśli stawiasz ten sam VirtualHost trzy razy w roku dla różnych projektów — generator oszczędza czas i wyłapuje literówki, które kosztują godzinę debugowania.
Praktyka — budujemy VirtualHosta krok po kroku
Działający VirtualHost w Apache na Ubuntu i Debian ma sześć elementów: port nasłuchu z ServerName, document root, uprawnienia katalogu, handler PHP, logi oraz (przy SSL) ścieżki do certyfikatu plus przekierowanie HTTP na HTTPS. Idziemy po nich w kolejności, w jakiej pojawiają się w pliku.
Porty i ServerName
Każdy VirtualHost zaczyna się od portu i nazwy hosta, na które odpowiada:
<VirtualHost *:80>
ServerName www.przyklad.pl
ServerAlias przyklad.pl
...
</VirtualHost>
*:80 oznacza „dowolny adres IP na porcie 80". Na serwerze z jednym IP to jest to, co chcesz. ServerName to kanoniczna nazwa hosta; ServerAlias to wszystkie inne, które mają trafiać na ten sam vhost. Jeśli i przyklad.pl, i www.przyklad.pl mają wskazywać tę samą stronę — daj jedną jako ServerName, drugą jako ServerAlias. Konwencja na większości produkcyjnych stron to kanonikalizacja do www (albo do gołej domeny — wybierz jedno, drugie przekieruj), dlatego generator produkuje 301 redirect z przyklad.pl na www.przyklad.pl.
Przy SSL vhost na porcie 80 nic nie robi poza przekierowaniem na HTTPS:
<VirtualHost *:80>
ServerName www.przyklad.pl
ServerAlias przyklad.pl
Redirect permanent / https://www.przyklad.pl/
</VirtualHost>
Cała treść mieszka w bloku *:443.
DocumentRoot i uprawnienia katalogu
DocumentRoot to miejsce, w którym leżą pliki strony. Konwencja na systemach z rodziny Debiana to /var/www/{domena}/public_html:
DocumentRoot /var/www/www.przyklad.pl/public_html
<Directory "/var/www/www.przyklad.pl/public_html/">
Options FollowSymLinks
AllowOverride All
Require all granted
</Directory>
Trzy rzeczy o bloku <Directory>:
Options FollowSymLinkspozwala na linki symboliczne wewnątrz document root. Nie dodawajIndexes, chyba że świadomie chcesz, by Apache pokazał listing katalogu przy brakuindex.html— większość stron nie powinna tego mieć.AllowOverride Allwłącza obsługę.htaccessw document root. Frameworki w stylu Laravel czy CMS-y typu WordPress zależą od tego dla URL rewriting. Jeśli nie używasz.htaccess, przełącz naAllowOverride Nonedla drobnego zysku wydajności.Require all grantedto nowoczesna dyrektywa kontroli dostępu (Apache 2.4+). Jeśli kopiujesz stare konfiguracje zOrder allow,denyiAllow from all— zamień. Tamta składnia jest przestarzała od ponad dekady i w przyszłych wersjach może przestać działać.
Handler PHP-FPM
Współczesne PHP w Apache to PHP-FPM, nie mod_php. Dwa powody: pule PHP per-strona (różni użytkownicy, różne limity pamięci, różne wersje PHP per vhost) oraz obsługa HTTP/2 — moduł HTTP/2 Apache wymaga MPM Event, niekompatybilnego z mod_php.
Blok handlera mówi Apache, żeby przekierowywał pliki .php przez gniazdo Unix do procesu master PHP-FPM:
<FilesMatch \.php$>
SetHandler "proxy:unix:/run/php/php8.4-fpm.sock|fcgi://localhost"
</FilesMatch>
Dostosuj wersję (php8.3-fpm.sock, php7.4-fpm.sock itd.) do tego, co masz uruchomione. Sprawdź:
ls -la /run/php/
systemctl status php8.4-fpm
Żeby to działało, musisz mieć włączone dwa moduły Apache — proxy i proxy_fcgi:
sudo a2enmod proxy proxy_fcgi
sudo systemctl restart apache2
Jeśli z powodów legacy zostajesz przy mod_php, pomiń blok <FilesMatch> w całości — mod_php obsługuje pliki PHP natywnie. Opcja apache-mod w generatorze robi dokładnie to.
Logi — wspólne czy per-vhost
Domyślnie Apache pisze do współdzielonego error.log i access.log:
ErrorLog ${APACHE_LOG_DIR}/error.log
CustomLog ${APACHE_LOG_DIR}/access.log combined
Dla jednej-dwóch stron to wystarczy, dla dziesięciu jest udręką. Osobne logi per-vhost znacznie ułatwiają debugowanie i rotację:
ErrorLog ${APACHE_LOG_DIR}/www.przyklad.pl-error.log
CustomLog ${APACHE_LOG_DIR}/www.przyklad.pl-access.log combined
${APACHE_LOG_DIR} to /var/log/apache2 na Ubuntu i Debianie — zdefiniowane w /etc/apache2/envvars. Format combined zawiera referrer i User-Agent na bazie standardowych pól CLF. Jeśli planujesz wysyłać logi do Loki albo Elasticsearch, rozważ przejście na JSON LogFormat — składnia opisana w dokumentacji mod_log_config.
SSL z Let's Encrypt
Dla HTTPS konfiguracja dorzuca drugiego VirtualHosta na porcie 443 z trzema dyrektywami SSL:
SSLEngine On
SSLCertificateFile /etc/letsencrypt/live/przyklad.pl/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/przyklad.pl/privkey.pem
Ścieżka /etc/letsencrypt/live/przyklad.pl/ to miejsce, gdzie certbot trzyma certyfikaty po wykonaniu:
certbot certonly --agree-tos --email root@przyklad.pl --webroot \
-w /var/lib/letsencrypt/ \
-d przyklad.pl -d www.przyklad.pl
To wystawia jeden certyfikat pokrywający obie nazwy przez pole SAN (Subject Alternative Name). Nowoczesne przeglądarki ignorują Common Name certyfikatu i sprawdzają zgodność z SAN, więc ten sam plik obsługuje i gołą domenę, i wariant z www. Nazwa katalogu w /etc/letsencrypt/live/ pochodzi od pierwszego -d w poleceniu — utrzymuj spójność z SSLCertificateFile, bo inaczej odnowienie po cichu podmieni nie ten certyfikat.
Wewnątrz vhosta :443 jest też mały redirect kanonikalizujący ruch:
<If "%{HTTP_HOST} == 'przyklad.pl'">
Redirect permanent / https://www.przyklad.pl/
</If>
Wymusza, żeby odwiedzający, którzy trafili na gołą domenę po HTTPS, lądowali na wariancie z www. Wyszukiwarki widzą wtedy jeden kanoniczny URL, nie dwa.
Wzmocnienie bezpieczeństwa
Generator zawiera opcjonalny blok hardening, który powinien być w każdym vhoście wystawionym publicznie:
# Hide Apache version
ServerSignature Off
ServerTokens Prod
# Block access to .git, .env, .htaccess etc. (allow .well-known for ACME)
<DirectoryMatch "^/.*/\.git/">
Require all denied
</DirectoryMatch>
<FilesMatch "^\.(?!well-known)">
Require all denied
</FilesMatch>
ServerSignature Off i ServerTokens Prod razem ukrywają wersję Apache i system operacyjny w nagłówkach odpowiedzi i stronach błędów. Atakujący zbierają fingerprinty wersji, żeby dobrać exploity — informacja „Apache/2.4.41 (Ubuntu)" to darmowy wywiad.
Dwa bloki <DirectoryMatch> i <FilesMatch> blokują najczęstsze wpadki: pozostawione .git/ w document root po deployu przez git clone oraz zacommitowane pliki .env z hasłami do bazy. Negatywny lookahead ^\.(?!well-known) blokuje wszystkie pliki kropkowe oprócz .well-known/, którego ACME challenge Let's Encrypt potrzebuje do odnawiania certyfikatów. Bez tego wyjątku następne odnowienie skończy się błędem 403.
Buforowanie statycznych plików
Generator oferuje też opcję Buforowanie popularnych typów plików: tak/nie, która dorzuca dwa bloki:
<IfModule mod_deflate.c>
AddOutputFilterByType DEFLATE text/html text/plain text/xml text/css application/javascript application/json
</IfModule>
<IfModule mod_expires.c>
ExpiresActive On
ExpiresByType image/jpeg "access plus 1 month"
ExpiresByType image/png "access plus 1 month"
ExpiresByType image/webp "access plus 1 month"
ExpiresByType text/css "access plus 1 week"
ExpiresByType application/javascript "access plus 1 week"
</IfModule>
mod_deflate włącza kompresję gzip w locie dla odpowiedzi tekstowych — typowo 60–80% redukcji rozmiaru dla HTML, CSS i JSON. mod_expires ustawia nagłówki Cache-Control i Expires, dzięki czemu przeglądarki nie pobierają niezmienionych obrazków i CSS przy każdym wejściu na stronę. Oba moduły są w standardzie Apache i wymagają jednorazowego włączenia globalnie:
sudo a2enmod deflate expires
sudo systemctl restart apache2
Pasuje do typowych stron i CMS-ów. Dla API albo panelu admina, który serwuje dynamiczny JSON na każde żądanie, zostaw wyłączone — nie chcesz, żeby klienci cache'owali odpowiedzi, które powinny być pobierane na nowo.
Włączanie strony
Plik zapisany jako /etc/apache2/sites-available/www.przyklad.pl.conf aktywuje się tak:
sudo a2ensite www.przyklad.pl.conf
sudo apache2ctl configtest && sudo apache2ctl graceful
configtest parsuje konfigurację i zwraca Syntax OK (na stderr, nie stdout — pamiętaj o 2>&1, jeśli chcesz pipe'ować) albo precyzyjny błąd z numerem linii. graceful przeładowuje workery bez zerwania trwających requestów — tego chcesz na serwerze produkcyjnym. service apache2 status potwierdza, że Apache wstał.
Pełny katalog dyrektyw Apache 2.4 na httpd.apache.org — zakładka warta odznaczenia.
Częste błędy i pułapki
To są błędy, które zjadają najwięcej czasu, kiedy coś nie działa.
AH00558: Could not reliably determine the server's fully qualified domain name
Ten warning pojawia się przy każdym restarcie i wygląda groźnie. Nie jest — Apache nadal obsługuje ruch. Przyczyna to brak globalnego ServerName w głównym apache2.conf. Dodaj jedną linię:
ServerName 127.0.0.1
do /etc/apache2/apache2.conf i warning znika.
PHP-FPM po cichu wraca do mod_php
Skonfigurowałeś PHP-FPM, ale phpinfo() pokazuje Server API: Apache 2.0 Handler. Prawie zawsze przyczyną jest pozostawiony aktywny mod_php. Wyłącz go jawnie:
sudo a2dismod php8.4
sudo a2enmod proxy proxy_fcgi
sudo a2enmod mpm_event
sudo a2dismod mpm_prefork
sudo systemctl restart apache2
mpm_prefork i mod_php chodzą w parze — oba muszą zniknąć, żeby PHP-FPM przejął.
Odnowienie certbota kończy się błędem Failed to connect to host for DVSNI challenge
Proces odnawiania potrzebuje zapisać tymczasowy plik do /var/lib/letsencrypt/.well-known/acme-challenge/ i wystawić go przez HTTP. Jeśli Twój blok hardening blokuje wszystkie pliki kropkowe bez wyjątku dla .well-known, challenge nie przechodzi. Fix to wzorzec z negatywnym lookahead:
<FilesMatch "^\.(?!well-known)">
Require all denied
</FilesMatch>
Permission denied na DocumentRoot
Apache loguje client denied by server configuration: /var/www/przyklad.pl/public_html. Dwie częste przyczyny: brak bloku <Directory> albo blok z Require all denied, lub uprawnienia systemu plików nie pozwalają www-data czytać katalogu. Sprawdź:
sudo -u www-data ls /var/www/przyklad.pl/public_html/
Jeśli to się wywala, popraw właściciela:
sudo chown -R www-data:www-data /var/www/przyklad.pl/
sudo chmod -R 755 /var/www/przyklad.pl/
a2ensite przechodzi, ale strona nie odpowiada
Apache ładuje pliki z /etc/apache2/sites-enabled/ alfabetycznie, a pierwszy pasujący VirtualHost wygrywa dla niedopasowanych hostów. Jeśli Twoja nazwa pliku zaczyna się od cyfry mniejszej niż 000-default.conf, może przypadkiem zostać catch-allem. Trzymaj się konwencji {servername}.conf — sortuje się po 000-default.conf i zachowuje przewidywalnie.
SSL działa na www, ale goła domena zwraca warning o certyfikacie
Prawie zawsze brakuje -d w komendzie certbota. Certyfikat musi zawierać obie nazwy w SAN. Sprawdź:
openssl x509 -in /etc/letsencrypt/live/przyklad.pl/fullchain.pem -noout -text | grep -A1 "Subject Alternative Name"
Jeśli widać tylko jedną domenę, wystaw na nowo z obiema:
certbot certonly --agree-tos --email root@przyklad.pl --webroot \
-w /var/lib/letsencrypt/ \
-d przyklad.pl -d www.przyklad.pl
Przeglądarka pokazuje stare treści po deployu
mod_expires robi swoją robotę — i to za dobrze. Jeśli ustawiłeś CSS na "access plus 1 week" i użytkownik odwiedził stronę w poniedziałek, nie zobaczy wtorkowej aktualizacji do następnego poniedziałku. Używaj cache busting w nazwach (style.abc123.css) albo query stringów (style.css?v=2026.05.01) dla zasobów, które się zmieniają. Statyczne pliki, które się nie zmieniają (np. /favicon.ico), mogą zostać z długim cache.
Edycja /etc/apache2/sites-enabled/ zamiast sites-available/
Pliki w sites-enabled/ to symlinki. Edycja ich edytuje oryginały — to OK, ale tworzenie nowych plików tam łamie workflow a2ensite/a2dissite. Zawsze twórz konfiguracje w sites-available/ i symlinkuj przez a2ensite. Oszczędza godziny debugowania w stylu „dlaczego to się nie wyłącza".
FAQ — najczęściej zadawane pytania
Czy potrzebuję osobnego pliku .conf dla każdej domeny?
Tak, to standardowy wzorzec. Jeden plik na ServerName utrzymuje konfigurację uporządkowaną, czyni a2ensite/a2dissite prostym i izoluje błędy — błąd składni w jednym vhoście nie kładzie innych. Generator nazywa pliki {servername}.conf, więc mapują się 1:1 na domenę.
Czy mogę umieścić port 80 i 443 w tym samym pliku?
Tak, i to jest rekomendowany wzorzec. Generator produkuje oba VirtualHosty w jednym pliku .conf, kiedy włączone jest SSL — blok na porcie 80 przekierowuje na HTTPS, blok na 443 zawiera właściwą konfigurację. Wszystko związane z jedną domeną w jednym miejscu.
Jak uruchomić wiele wersji PHP na tym samym serwerze?
Zainstaluj wiele paczek PHP-FPM obok siebie (php7.4-fpm, php8.3-fpm, php8.4-fpm — najczęściej przez PPA ondrej/php) i wskaż każdy VirtualHost na odpowiednią wersję, zmieniając ścieżkę socketu w <FilesMatch>. Każda wersja PHP ma swój master process i pulę — nie kolidują ze sobą.
Czym różni się apache2ctl reload od apache2ctl graceful?
reload re-czyta konfigurację, ale ubija aktywne workery — może to zerwać trwające requesty. graceful pozwala aktywnym workerom dokończyć bieżący request przed wymianą. Na produkcji zawsze używaj graceful.
FollowSymLinks czy SymLinksIfOwnerMatch?
SymLinksIfOwnerMatch jest bezpieczniejsze — Apache podąża za symlinkiem tylko wtedy, gdy link i target mają tego samego właściciela, co zapobiega wyciekowi z zapisywalnego katalogu do chronionych obszarów. Minus to drobny narzut wydajnościowy (Apache wywołuje stat() na każdym linku). Dla typowych serwerów single-tenant FollowSymLinks jest OK. W hostingu współdzielonym SymLinksIfOwnerMatch to bezpieczniejszy domyślny wybór.
Czy muszę restartować Apache po certbot renew?
Nie. Hooki certbota (/etc/letsencrypt/renewal-hooks/deploy/) obsługują reload Apache, kiedy certyfikat zostaje wymieniony. Restart ręczny jest potrzebny tylko wtedy, gdy zmieniasz samą konfigurację vhosta.
Czy mogę używać jednego VirtualHosta dla wielu subdomen?
Tak — wymień każdą subdomenę w ServerAlias:
ServerName przyklad.pl
ServerAlias www.przyklad.pl app.przyklad.pl api.przyklad.pl
Dla różnych document rootów per subdomena — twórz osobne VirtualHosty. Dla tej samej treści pod wieloma nazwami wystarczy jeden vhost z aliasami.
Dlaczego apache2ctl configtest zwraca Syntax OK, a strona dalej nie działa?
configtest sprawdza tylko składnię — nie waliduje zachowania w trakcie pracy. Konfiguracja może być składniowo poprawna, ale odwoływać się do nieistniejącego pliku (literówka w SSLCertificateFile), niezaładowanego modułu (SetHandler dla proxy_fcgi bez a2enmod proxy_fcgi) albo niedostępnego socketu PHP-FPM. Sprawdź error.log po graceful reload, żeby złapać takie przypadki.
Następne kroki
Wygeneruj swojego pierwszego VirtualHosta w generatorze konfiguracji Apache na vps-web.com, wrzuć do /etc/apache2/sites-available/, odpal a2ensite i apache2ctl graceful — masz działającą stronę w niecałe pięć minut.
Powiązane tematy, które rozszerzają realny workflow backupowy:
- Generator serwera DNS Bind 9 — konfiguracja DNS to kolejny naturalny cel offsite-owego rsync.
Cała konfiguracja od początku do końca przeszła przez film na moim kanale YouTube — z pułapkami wokół PHP-FPM, odnawiania certbota i pokazem na żywo, co dokładnie blokuje każdy blok bezpieczeństwa. Subskrybuj, jeśli chcesz mieć następne tutoriale w feedzie.