vps·web
PL

Konfiguracja Apache VirtualHost

Konfiguracja Apache VirtualHost na Ubuntu — krok po kroku

Serwery · Konfiguracja Apache VirtualHost

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 FollowSymLinks pozwala na linki symboliczne wewnątrz document root. Nie dodawaj Indexes, chyba że świadomie chcesz, by Apache pokazał listing katalogu przy braku index.html — większość stron nie powinna tego mieć.
  • AllowOverride All włącza obsługę .htaccess w 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 na AllowOverride None dla drobnego zysku wydajności.
  • Require all granted to nowoczesna dyrektywa kontroli dostępu (Apache 2.4+). Jeśli kopiujesz stare konfiguracje z Order allow,deny i Allow 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:

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.