Logo David Burdelak
Webdevelopment

Przejście z XAMPP-a na Dockera w projektach webowych

Docker

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

AspektXAMPPDocker
Wersja PHPjedna wspólna dla wszystkich projektówosobna dla każdego projektu, ustawiana w jednej linii
Rozszerzenia PHPwłączane ręcznie w php.inizapisane w pliku Dockerfile w repozytorium
SystemWindows lub macOSLinux w kontenerze, jak na większości serwerów
Portystałe 80 i 3306, konflikty przy kilku projektachkażdy projekt na własnym porcie, np. 8080, 8081
Kilka projektów narazwspólny serwer i wspólna bazaniezależne zestawy kontenerów
Przekazanie projektuinstrukcja instalacji i ręczna konfiguracjarepozytorium i jedno polecenie docker compose up
Próg wejściainstalator i panel z przyciskamipodstawy 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

  1. Eksport bazy. Ostatni raz uruchamiam XAMPP-a i eksportuję bazę projektu do pliku SQL przez phpMyAdmin albo poleceniem mysqldump.
  2. Struktura katalogów. Tworzę katalog projektu, pliki z htdocs przenoszę do src, a plik SQL do db-init.
  3. Konfiguracja. Dodaję pliki docker-compose.yml, Dockerfile i .env z wersją PHP taką jak na produkcji.
  4. Uruchomienie. Wpisuję docker compose up -d --build. Przy pierwszym starcie baza automatycznie importuje plik SQL z katalogu db-init.
  5. Połączenie z bazą. W konfiguracji strony, np. w wp-config.php, zmieniam host bazy z localhost na nazwę usługi, czyli db.
  6. Adresy w bazie. Gdy strona zapisuje pełne adresy w bazie, jak WordPress, podmieniam stary adres lokalny na http://localhost:8080, np. poleceniem wp search-replace, które poprawnie obsługuje dane zserializowane.
  7. 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:

DaneGdzie je trzymamDlaczego
kod stronykatalog projektu zamontowany w kontenerze (./src)edytuję pliki w edytorze, a zmiany są od razu widoczne w przeglądarce
baza danychnazwany wolumen Dockera (db-data)baza przetrwa przebudowę i usunięcie kontenerów, a Docker zarządza uprawnieniami plików
zrzut bazy do importukatalog db-initnowa 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

ObjawPrzyczynaCo robię
błąd połączenia z bazą danychhost bazy ustawiony na localhostzmieniam host na nazwę usługi, np. db
podstrony zwracają błąd 404, działa tylko strona głównawyłączony mod_rewritewłą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 kontenerzeustawiam właściciela katalogu z plikami przesyłanymi na użytkownika www-data
strona działa bardzo wolno na Windowsiepliki projektu leżą na dysku Windowsa, a nie w systemie plików WSLtrzymam projekty w katalogu domowym dystrybucji Linuksa w WSL
port jest już zajętyinny projekt lub stary XAMPP używa tego samego portuzmieniam port w docker-compose.yml i wyłączam autostart XAMPP-a
baza nie zaimportowała pliku SQLwolumen bazy już istniał, a import działa tylko przy pierwszym starcieimportuję 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.example do .env i 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.