Przez lata pracowałem lokalnie na XAMPP-ie i przy jednym prostym projekcie nie miałem z nim problemów. Kłopoty zaczynały się, gdy
równolegle prowadziłem kilka stron. Każda wymagała innej wersji PHP i innych rozszerzeń, porty się blokowały, a strona działająca lokalnie
po wdrożeniu zwracała błąd 500, bo na serwerze brakowało modułu albo baza miała inne ustawienia. Dziś każdy projekt uruchamiam w Dockerze
z jednego pliku konfiguracyjnego, który trzymam w repozytorium. W tym wpisie pokazuję, jak przenoszę stronę z XAMPP-a do kontenerów,
jaki plik docker-compose.yml stosuję w projektach PHP i jakie błędy najczęściej pojawiają się przy migracji.
Dlaczego odszedłem od XAMPP-a
XAMPP instaluje Apache, PHP i bazę danych bezpośrednio w systemie, jako jeden wspólny zestaw dla wszystkich projektów. W praktyce oznaczało to dla mnie:
- jedną wersję PHP dla wszystkich stron naraz, choć klienci mieli na serwerach różne wersje,
- ręczne włączanie rozszerzeń w
php.ini, o których po roku nikt nie pamiętał przy wdrożeniu, - serwer działający na Windowsie lub macOS, czyli inny system plików i inną wielkość liter w nazwach plików niż na serwerze z Linuksem,
- konfigurację, której nie dało się przekazać drugiej osobie inaczej niż instrukcją „zainstaluj to i to”.
W Dockerze każdy projekt ma własne kontenery z Linuksem, własną wersję PHP i własną bazę. Na Windowsie i macOS Docker uruchamia je w lekkiej maszynie wirtualnej, więc środowisko lokalne jest bardzo zbliżone do serwera produkcyjnego, choć nie identyczne. Konfiguracja serwera, wydajność dysku czy ustawienia hostingu nadal mogą się różnić.
XAMPP a Docker
| Aspekt | XAMPP | Docker |
|---|---|---|
| Wersja PHP | jedna wspólna dla wszystkich projektów | osobna dla każdego projektu, ustawiana w jednej linii |
| Rozszerzenia PHP | włączane ręcznie w php.ini | zapisane w pliku Dockerfile w repozytorium |
| System | Windows lub macOS | Linux w kontenerze, jak na większości serwerów |
| Porty | stałe 80 i 3306, konflikty przy kilku projektach | każdy projekt na własnym porcie, np. 8080, 8081 |
| Kilka projektów naraz | wspólny serwer i wspólna baza | niezależne zestawy kontenerów |
| Przekazanie projektu | instrukcja instalacji i ręczna konfiguracja | repozytorium i jedno polecenie docker compose up |
| Próg wejścia | instalator i panel z przyciskami | podstawy pracy w terminalu i plików konfiguracyjnych |
Mój plik docker-compose.yml dla projektu PHP
Przy stronach na PHP, w tym na WordPressie, zaczynam od trzech usług: serwera z PHP i Apache, bazy danych i phpMyAdmin. Apache wybieram wtedy, gdy produkcja też działa na Apache albo LiteSpeed, bo obsługuje reguły z pliku .htaccess tak samo jak serwer docelowy.
services:
app:
build:
context: .
args:
PHP_VERSION: ${PHP_VERSION}
ports:
- "127.0.0.1:8080:80"
volumes:
- ./src:/var/www/html
depends_on:
- db
db:
image: mariadb:lts
environment:
MARIADB_DATABASE: ${DB_NAME}
MARIADB_USER: ${DB_USER}
MARIADB_PASSWORD: ${DB_PASSWORD}
MARIADB_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
volumes:
- db-data:/var/lib/mysql
- ./db-init:/docker-entrypoint-initdb.d
phpmyadmin:
image: phpmyadmin
ports:
- "127.0.0.1:8081:80"
environment:
PMA_HOST: db
depends_on:
- db
volumes:
db-data:
Rozszerzenia PHP, których nie ma w obrazie bazowym, dopisuję w pliku Dockerfile. Od razu włączam też moduł
mod_rewrite, bez którego nie działają przyjazne adresy i reguły z .htaccess:
ARG PHP_VERSION
FROM php:${PHP_VERSION}-apache
RUN apt-get update && apt-get install -y libzip-dev libpng-dev libjpeg-dev libwebp-dev libicu-dev \
&& docker-php-ext-configure gd --with-jpeg --with-webp \
&& docker-php-ext-install mysqli pdo_mysql gd zip intl opcache \
&& a2enmod rewrite headers expires
Wersję PHP i dane dostępowe trzymam w pliku .env, który nie trafia do repozytorium. Do repozytorium dodaję tylko jego wzór
bez haseł, np. .env.example:
# ta sama wersja PHP co na serwerze produkcyjnym
PHP_VERSION=8.3
DB_NAME=projekt
DB_USER=projekt
DB_PASSWORD=haslo-lokalne
DB_ROOT_PASSWORD=haslo-root-lokalne
Kilka decyzji w tej konfiguracji wynika z problemów, które miałem wcześniej:
- Porty przypisane do
127.0.0.1. Bez tego baza i phpMyAdmin są dostępne dla każdego w tej samej sieci, np. w kawiarni czy w biurze coworkingowym. - Wersja PHP w jednym miejscu. Gdy hosting aktualizuje PHP, zmieniam jedną linię i od razu sprawdzam projekt na nowej wersji, zanim zmiana trafi na produkcję.
- phpMyAdmin jako osobny kontener. Mam ten sam interfejs, do którego jestem przyzwyczajony z XAMPP-a, ale przypisany do konkretnego projektu i jego bazy.
Jak przenoszę projekt z XAMPP-a
- Eksport bazy. Ostatni raz uruchamiam XAMPP-a i eksportuję bazę projektu do pliku SQL przez phpMyAdmin albo poleceniem
mysqldump. - Struktura katalogów. Tworzę katalog projektu, pliki z
htdocsprzenoszę dosrc, a plik SQL dodb-init. - Konfiguracja. Dodaję pliki
docker-compose.yml,Dockerfilei.envz wersją PHP taką jak na produkcji. - Uruchomienie. Wpisuję
docker compose up -d --build. Przy pierwszym starcie baza automatycznie importuje plik SQL z katalogudb-init. - Połączenie z bazą. W konfiguracji strony, np. w
wp-config.php, zmieniam host bazy zlocalhostna nazwę usługi, czylidb. - Adresy w bazie. Gdy strona zapisuje pełne adresy w bazie, jak WordPress, podmieniam stary adres lokalny na
http://localhost:8080, np. poleceniemwp search-replace, które poprawnie obsługuje dane zserializowane. - Sprawdzenie. Przechodzę przez kluczowe podstrony, formularze i panel administracyjny, a logi przeglądam poleceniem
docker compose logs -f app.
Te same zasady, czyli kopia bazy, podmiana adresów i lista kontrolna po przeniesieniu, stosuję przy każdej migracji strony internetowej między serwerami.
Gdzie zapisują się pliki i baza
Kontener jest tymczasowy. Wszystko, co zapisze się wyłącznie w jego wnętrzu, znika po jego przebudowie. Dlatego dane dzielę na dwa rodzaje miejsc:
| Dane | Gdzie je trzymam | Dlaczego |
|---|---|---|
| kod strony | katalog projektu zamontowany w kontenerze (./src) | edytuję pliki w edytorze, a zmiany są od razu widoczne w przeglądarce |
| baza danych | nazwany wolumen Dockera (db-data) | baza przetrwa przebudowę i usunięcie kontenerów, a Docker zarządza uprawnieniami plików |
| zrzut bazy do importu | katalog db-init | nowa osoba w zespole dostaje bazę testową przy pierwszym uruchomieniu |
Trzeba pamiętać o jednym poleceniu: docker compose down -v usuwa razem z kontenerami także wolumeny, czyli całą lokalną bazę.
Przed takim czyszczeniem zawsze robię zrzut bazy.
Najczęstsze problemy po przejściu na Dockera
| Objaw | Przyczyna | Co robię |
|---|---|---|
| błąd połączenia z bazą danych | host bazy ustawiony na localhost | zmieniam host na nazwę usługi, np. db |
| podstrony zwracają błąd 404, działa tylko strona główna | wyłączony mod_rewrite | włączam moduł w Dockerfile i przebudowuję obraz |
| wtyczka nie może zapisać plików, brak miniatur zdjęć | uprawnienia katalogu nie pasują do użytkownika serwera w kontenerze | ustawiam właściciela katalogu z plikami przesyłanymi na użytkownika www-data |
| strona działa bardzo wolno na Windowsie | pliki projektu leżą na dysku Windowsa, a nie w systemie plików WSL | trzymam projekty w katalogu domowym dystrybucji Linuksa w WSL |
| port jest już zajęty | inny projekt lub stary XAMPP używa tego samego portu | zmieniam port w docker-compose.yml i wyłączam autostart XAMPP-a |
| baza nie zaimportowała pliku SQL | wolumen bazy już istniał, a import działa tylko przy pierwszym starcie | importuję ręcznie albo usuwam wolumen po zrobieniu kopii |
Gdy problem dotyczy samego kodu, a nie środowiska, do kontenera dokładam Xdebug, bo krokowe
debugowanie kodu JavaScript i PHP działa w Dockerze tak samo jak lokalnie. Logi Apache i PHP czytam bezpośrednio
z docker compose logs, bez szukania plików w katalogach instalacji.
Co zyskałem na przejściu
- Mniej niespodzianek przy wdrożeniu. Wersja PHP i rozszerzenia są zapisane w repozytorium, a nie w pamięci osoby, która konfigurowała komputer.
- Szybkie wejście do projektu. Nowa osoba klonuje repozytorium, kopiuje
.env.exampledo.envi uruchamia jedno polecenie. Konfiguracja przechodzi przegląd jak każda inna zmiana, tak jak przy pracy z Gitem w zespole. - Testy aktualizacji przed hostingiem. Nową wersję PHP sprawdzam lokalnie, zanim hosting wymusi ją na serwerze.
- Porządek w systemie. Po zakończeniu projektu usuwam jego kontenery i obrazy, a w systemie nie zostają żadne serwery ani bazy.
XAMPP nadal wystarcza do szybkiej nauki PHP na jednym komputerze. Przy pracy zawodowej, kilku projektach naraz i współpracy w zespole wybieram Dockera, bo konfiguracja środowiska staje się częścią projektu, a nie wiedzą jednej osoby.