Jak zacząć z Dockerem w PHP – praktyczny przewodnik dla webmasterów

0
28
3/5 - (2 votes)

Z tego artykuły dowiesz się:

Dlaczego Docker ma sens dla webmastera pracującego z PHP

Kontener a wirtualna maszyna – obrazowe porównanie

Kontener w Dockerze najłatwiej traktować jak izolowaną aplikację z jej mini-systemem, a nie jak pełny system operacyjny. Wirtualna maszyna (VM) uruchamia cały system w systemie: ma własne jądro, sterowniki, pełny zestaw usług. Kontener korzysta z jądra hosta, ale ma własne procesy, biblioteki i konfigurację.

W praktyce oznacza to trzy rzeczy istotne dla webmastera:

  • start w sekundach, a nie minutach – kontener z PHP + Nginx podnosi się prawie natychmiast, więc restart środowiska nie wybija z rytmu pracy,
  • mniejszy narzut – mniej RAM i CPU niż pełne maszyny wirtualne, dzięki czemu można mieć równolegle kilka projektów,
  • łatwe kopiowanie – kontener to powtarzalny zestaw kroków (image), a nie ręcznie „skonfigurowany serwer”, którego boisz się dotknąć.

Dla kogoś, kto do tej pory stawiał XAMPP czy własny LAMP na laptopie, Docker jest czymś pomiędzy: daje wygodę „gotowego pakietu” i elastyczność ręcznej konfiguracji, ale spakowaną w przenośne obrazy.

Typowe bolączki webmastera bez Dockera

Przy pracy z wieloma projektami PHP szybko wychodzą na wierzch powtarzalne problemy:

  • jeden projekt wymaga PHP 7.4, inny już PHP 8.2, a system ma zainstalowaną tylko jedną wersję,
  • WordPress potrzebuje innej wersji MySQL niż aplikacja w Laravelu, pojawiają się konflikty konfiguracji,
  • na Twoim komputerze „wszystko działa”, ale u kolegi z zespołu wywala się na rozszerzeniu gd czy intl,
  • aktualizacja lokalnego PHP rozwala stary legacy projekt, którego nikt nie chce ruszać.

Docker rozwiązuje to poprzez izolację środowisk. Każdy projekt ma swój obraz PHP, swoje rozszerzenia, własny serwer bazy danych, własne konfiguracje. System hosta jest praktycznie „czysty”, potrzebny jest głównie do uruchomienia Dockera i edytora kodu.

Korzyści z Dockera w codziennej pracy webmastera

Najważniejsza przewaga Dockera dla webmastera to powtarzalne środowisko. Ten sam plik Dockerfile i ten sam docker-compose.yml możesz:

  • uruchomić na Windowsie, macOS i Linuksie bez przepisywania instrukcji instalacji,
  • wysłać do współpracownika – wystarczy docker compose up, aby mieć identyczne środowisko,
  • włączyć w pipeline na serwerze CI/CD, aby testy robiły się w takim samym PHP jak lokalnie.

Dochodzi do tego szybkie stawianie nowych projektów. Zamiast kolejny raz instalować Apache, PHP, MySQL, tworzyć virtual hosty, wystarczy skopiować sprawdzony szablon projektu i zmienić kilka nazw w konfiguracji. Nowy projekt ma od razu sensowną strukturę, logowanie, bazy i sieci.

Przy pracy w zespole kontenery dają też prostszy onboarding. Nowa osoba nie spędza dnia na konfigurowaniu serwera lokalnego – odpala jedną komendę. To szczególnie cenne w firmach, które równolegle utrzymują starsze WordPressy i nowe headless aplikacje z API.

Przykład z życia: jeden projekt, wiele systemów

Wyobraź sobie projekt PHP (np. sklep na Laravelu), nad którym równolegle pracują trzy osoby: Ty na Windowsie, frontendowiec na macOS i administrator na Linuksie. Bez kontenerów każda z tych osób:

  • instaluje inną wersję PHP (choćby dlatego, że używa domyślnego menedżera pakietów),
  • ma inny plik php.ini i inne rozszerzenia,
  • różne ustawienia serwera WWW (vhosty, moduły, mod_rewrite).

Rezultat: ticket „nie działa” jest trudny do odtworzenia na innym komputerze. Docker spłaszcza ten problem. Serwis app, serwis db, wspólne wolumeny – to samo YAML-owe źródło uruchamiane na trzech systemach daje praktycznie identyczne środowisko, więc błędy są powtarzalne.

Podstawowe pojęcia Dockera w kontekście PHP (bez żargonu)

Obraz (image) – gotowy „zrzut” środowiska

Obraz Dockera można porównać do skompresowanej paczki ZIP z całym środowiskiem: systemem bazowym (np. Debian Slim), PHP, potrzebnymi bibliotekami i konfiguracją. Z obrazu tworzy się kontenery.

Obrazy mają tagi, np. php:8.2-apache czy php:8.1-fpm-alpine. Tagi pozwalają:

  • wskazać konkretną wersję PHP (7.4, 8.0, 8.1, 8.2…),
  • wybrać typ serwera (Apache, FPM pod Nginx),
  • zdecydować, czy chcesz system bazowy Debian czy np. lżejszy Alpine.

Do tworzenia własnego obrazu używa się pliku Dockerfile. Tam zapisujesz instrukcje: z jakiego bazowego obrazu korzystasz, jakie rozszerzenia PHP doinstalować, jakie katalogi skopiować.

Kontener – działający „egzemplarz” obrazu

Kontener to uruchomiony egzemplarz obrazu. Z jednego obrazu możesz odpalić dziesiątki kontenerów – każdy ma własny system plików, własne procesy, ale współdzieli warstwy obrazu.

W praktyce kontener to np. „serwer PHP-FPM z logami i aplikacją projektu X”. Możesz go:

  • stopować i startować,
  • kasować i odtwarzać z obrazu bez straty ważnych danych (jeśli używasz wolumenów),
  • podłączać do sieci Dockera, aby łączył się z innymi kontenerami (np. bazą danych).

Różnica między obrazem a kontenerem jest podobna do różnicy między instalatorem programu (obraz) a zainstalowaną instancją na dysku, która już działa (kontener).

Dockerfile, registry i oficjalne obrazy PHP

Dockerfile to tekstowy plik z listą kroków, jak zbudować obraz. Przykład prostego Dockerfile dla PHP z Apache:

FROM php:8.2-apache

RUN docker-php-ext-install pdo pdo_mysql

COPY . /var/www/html

Registry to „magazyn” obrazów. Domyślnym jest Docker Hub, skąd pobierasz m.in. php:8.2-apache, mysql:8, nginx:stable. W firmach często używa się prywatnych rejestrów.

Przy PHP masz do wyboru:

  • oficjalne obrazy PHP – utrzymywane przez zespół PHP, udokumentowane, stabilne,
  • obrazy społeczności (np. zintegrowane z Nginx, Supervisor) – szybkie na start, ale trzeba patrzeć na aktualność i bezpieczeństwo.

Do nauki i pracy produkcyjnej dużo bezpieczniejszym wyborem są oficjalne obrazy PHP i własny Dockerfile z dopasowanymi rozszerzeniami.

Warstwy w obrazach i wpływ na szybkość pracy

Każda instrukcja w Dockerfile (np. RUN, COPY, ADD) tworzy nową warstwę. Warstwy są cache’owane. Jeśli nie zmienisz górnych kroków w Dockerfile, Docker podczas budowania obrazu wykorzysta już istniejące warstwy.

To ma znaczenie dla wydajności:

  • czas budowania skraca się, jeśli częściej zmienia się kod niż instalowane pakiety,
  • obraz jest mniejszy, gdy nie powielasz zbędnych plików wewnątrz wielu warstw.

Przykładowy błąd: najpierw kopiujesz cały kod projektu, a dopiero później instalujesz PHP extensions i pakiety systemowe. Każda zmiana kodu unieważnia cache niżej. Lepsze podejście to najpierw instalacja stałych pakietów, dopiero potem kopiowanie aplikacji.

Docker Compose – ważniejsze narzędzie niż „goły” docker run

docker-compose (w nowej wersji wywoływany jako docker compose) to narzędzie, które pozwala zdefiniować całe środowisko w jednym pliku YAML. Dla webmastera to kluczowe, bo realne projekty rzadko ograniczają się do jednego kontenera.

Typowy projekt PHP zawiera:

  • serwis app – PHP-FPM lub PHP+Apache,
  • serwis db – MySQL/MariaDB,
  • czasem serwis cache – Redis/Memcached,
  • serwer nginx jako reverse proxy dla FPM.

Zamiast ręcznie klepać skomplikowane komendy docker run dla każdego z nich, opisujesz całość w docker-compose.yml, a potem używasz kilku prostych poleceń: docker compose up, docker compose down, docker compose logs. To jak „skrypt startowy” dla całego projektu.

Przygotowanie stanowiska pracy: instalacja Dockera i pierwsze komendy

Instalacja Dockera na Windows, macOS i Linux

Na desktopach standardem jest Docker Desktop, który zawiera silnik Dockera i przyjazny GUI. Szybki przegląd opcji:

  • Windows 10/11 – Docker Desktop wymaga włączonego WSL2. To lekka warstwa linuksowa, na której działa Docker. Po instalacji warto w Docker Desktop włączyć integrację z konkretną dystrybucją (np. Ubuntu w WSL2).
  • macOS – Docker Desktop instaluje się jak typowa aplikacja. Pod spodem uruchamiana jest lekka maszyna linuksowa. Konfigurujesz ilość RAM/CPU w ustawieniach.
  • Linux – na większości dystrybucji instaluje się sam silnik Dockera: docker-ce. Dla wygody możesz doinstalować też docker-compose-plugin. Nie jest potrzebny Docker Desktop.

Po instalacji warto dodać obecnego użytkownika do grupy docker (na Linuksie), aby nie używać cały czas sudo. Instrukcje krok po kroku są dostępne w oficjalnej dokumentacji Dockera dla każdej platformy.

Weryfikacja działania: podstawowe komendy

Po zainstalowaniu Dockera pierwszym krokiem jest sprawdzenie, czy demon działa:

docker version

Jeśli zobaczysz informacje o wersji klienta i serwera, jest dobrze. Następnie lista działających kontenerów:

docker ps

Na świeżej instalacji lista będzie pusta. Zobacz też, jakie obrazy masz na dysku:

docker images

Na tym etapie prawdopodobnie żadnych. To zaraz się zmieni przy pierwszym teście z obrazem PHP.

Pierwszy test z oficjalnym obrazem PHP

Dla „suchego” sprawdzenia zacznij od interaktywnego kontenera PHP bez serwera WWW:

docker run -it --rm php:8.2-cli php -v

Ta komenda:

  • ściągnie obraz php:8.2-cli (jeśli go jeszcze nie ma),
  • uruchomi kontener w trybie interaktywnym (-it),
  • od razu wykona php -v,
  • po zakończeniu usunie kontener (--rm).

Możesz też wejść do powłoki kontenera:

docker run -it --rm php:8.2-cli bash

W środku zobaczysz standardowe narzędzia Linuksa. To dobry sposób, aby poczuć, że kontener to „mała maszyna” z którą można pracować jak ze zwykłym serwerem SSH, ale uruchamianą lokalnie.

Ustawienia zasobów w Docker Desktop

Na Windowsie i macOS Docker Desktop uruchamia w tle maszynę linuksową. W jej konfiguracji warto od razu ustawić sensowny limit zasobów:

  • RAM – dla typowych projektów PHP z MySQL dobrze działa 4–6 GB,
  • CPU – 2–4 vCPU wystarczy na większość środowisk developerskich.

Zbyt niski limit RAM spowoduje, że bazy danych i PHP będą się „zacinały”. Zbyt wysoki – system hosta zacznie zamulać, bo Docker „zje” za dużo pamięci. Kilka testów z realnym projektem pozwoli znaleźć złoty środek.

Najprostsze środowisko PHP w Dockerze – krok po kroku

Koncepcja: jeden kontener z PHP i serwerem WWW

Na pierwszy kontakt z Dockerem w kontekście PHP najprościej zacząć od jednego kontenera zawierającego zarówno PHP, jak i serwer WWW (Apache). Oficjalny obraz php:8.2-apache ma to wszystko wbudowane.

Załóż katalog projektu:

mkdir php-docker-demo
cd php-docker-demo

Następnie utwórz prosty plik index.php jako punkt wejścia aplikacji:

<?php
phpinfo();

Prosty Dockerfile dla PHP + Apache

Potrzebny jest plik Dockerfile, który zdefiniuje obraz z PHP i Apache. Stwórz go w katalogu projektu z taką zawartością:

FROM php:8.2-apache

# Włącz przydatne moduły Apache, np. mod_rewrite dla ładnych URL-i
RUN a2enmod rewrite

# Skopiuj pliki projektu do katalogu widocznego dla Apache
COPY . /var/www/html

# Opcjonalnie ustaw prawa (w prostych projektach zwykle wystarczy domyślna konfiguracja)

Ten plik mówi Dockerowi: weź oficjalny obraz z PHP 8.2 i Apache, włącz podstawowy moduł do ładnych adresów i skopiuj bieżący kod do katalogu serwera WWW. Na tym etapie nie konfigurujesz jeszcze wirtualnych hostów ani HTTPS – chodzi tylko o start.

Budowanie obrazu i uruchomienie kontenera

Kiedy Dockerfile i index.php są gotowe, zbuduj obraz komendą:

docker build -t php-demo .

Po zakończeniu budowania możesz go uruchomić, mapując port HTTP na port hosta:

docker run --name php-demo -p 8080:80 php-demo

Przeglądarka pod adresem http://localhost:8080 powinna pokazać stronę z informacjami o PHP. Jeśli chcesz dalej pracować w tym samym kontenerze, tylko zrestartować serwer, zatrzymaj go i uruchom ponownie poleceniami docker stop php-demo oraz docker start php-demo.

Mapowanie kodu z hosta, żeby nie przebudowywać obrazu

W praktycznej pracy wygodniej jest nie kopiować kodu do obrazu za każdym razem, tylko podpiąć go jako katalog współdzielony. Dzięki temu każda zmiana w plikach PHP na komputerze jest widoczna natychmiast w kontenerze. Można to zrobić, uruchamiając kontener z dodatkową opcją:

W wielu firmach, które uczą nowoczesnego webmasteringu (np. Porady-IT), Docker stał się standardem, bo pozwala podpiąć do projektu kogoś z zewnątrz bez długiego „rozstawiania środowiska”.

docker run --name php-demo -p 8080:80 -v ${PWD}:/var/www/html php:8.2-apache

Tutaj korzystasz bezpośrednio z oficjalnego obrazu php:8.2-apache, a bieżący katalog z projektem jest montowany do /var/www/html. Na Windowsie ${PWD} bywa kłopotliwe; lepiej podać pełną ścieżkę, np. -v C:projektyphp-docker-demo:/var/www/html. To proste podejście pokazuje codzienny rytm pracy: edytujesz pliki lokalnie, odświeżasz przeglądarkę, a Docker pełni rolę „lokalnego serwera” z powtarzalną konfiguracją.

Po takim rozruchu przejście do bardziej złożonych układów z Docker Compose, osobną bazą danych czy Nginx jako proxy staje się kwestią dokładania klocków do znanego już schematu, zamiast uczenia się wszystkiego naraz od zera.

Nastolatek programuje na kilku laptopach w nowoczesnym biurze
Źródło: Pexels | Autor: olia danilevich

Docker Compose dla projektów PHP – składanie klocków w całość

Dlaczego w ogóle bawić się w Docker Compose?

Przy jednym kontenerze z PHP i Apache sytuacja jest prosta. Jednak gdy tylko dochodzi baza danych, cache, czasem dodatkowy serwis (np. kolejki), ręczne odpalanie wszystkiego pojedynczymi komendami docker run staje się męczarnią. Docker Compose porządkuje ten chaos w jednym pliku docker-compose.yml.

Możesz tam opisać:

  • jakie usługi (kontenery) tworzą środowisko,
  • jakie obrazy mają wykorzystać,
  • jakie porty, wolumeny i zmienne środowiskowe mają być ustawione,
  • w jakiej kolejności startują oraz jak się ze sobą komunikują.

Efekt? Zamiast listy instrukcji typu „najpierw odpal MySQL, potem Redis, potem PHP”, masz jedną komendę: docker compose up. To ogromna oszczędność nerwów, szczególnie gdy współdzielisz projekt z zespołem.

Minimalny plik docker-compose.yml dla prostego projektu PHP

Na początek wystarczy prosty układ: aplikacja PHP z Apache oraz baza danych MySQL. W katalogu projektu utwórz plik docker-compose.yml o takiej treści:

version: "3.9"

services:
  app:
    image: php:8.2-apache
    container_name: php-demo-app
    ports:
      - "8080:80"
    volumes:
      - ./:/var/www/html
    depends_on:
      - db

  db:
    image: mysql:8.0
    container_name: php-demo-db
    environment:
      - MYSQL_ROOT_PASSWORD=root
      - MYSQL_DATABASE=app
      - MYSQL_USER=app
      - MYSQL_PASSWORD=app
    ports:
      - "3306:3306"
    volumes:
      - db_data:/var/lib/mysql

volumes:
  db_data:

Najważniejsze rzeczy, które tu się dzieją:

  • services.app – kontener z PHP i Apache, wystawiony na porcie 8080 hosta, zmontowany katalog projektu,
  • services.db – MySQL 8 z prostą konfiguracją hasła i bazy startowej,
  • volumes.db_data – nazwany wolumen przechowujący dane bazy poza życiem pojedynczego kontenera.

Taki plik wystarczy, aby w kilka sekund mieć gotowe środowisko do zabawy z PDO czy mysqli.

Uruchamianie i zatrzymywanie środowiska z Compose

Gdy docker-compose.yml jest gotowy, w katalogu projektu użyj podstawowych komend:

docker compose up

Środowisko wystartuje i logi wszystkich usług pojawią się w jednym strumieniu w terminalu. Gdy chcesz uruchomić wszystko w tle (jak „demon”), użyj:

docker compose up -d

Kontenery dalej działają, ale terminal pozostaje wolny. Zatrzymanie odbywa się jednym ruchem:

docker compose down

Domyślnie wolumen z danymi bazy (db_data) zostaje, więc po następnym starcie dane nadal będą dostępne. Jeśli zechcesz wyczyścić wszystko (np. przy testach), dodaj:

docker compose down -v

Łączenie się z bazą danych z poziomu PHP w Compose

Wewnątrz sieci Dockera usługi komunikują się po nazwach serwisów. Zamiast localhost używasz nazwy z sekcji services, czyli tutaj db. Przykładowe połączenie PDO w pliku index.php może wyglądać tak:

<?php
$dsn = 'mysql:host=db;dbname=app;charset=utf8mb4';
$user = 'app';
$pass = 'app';

try {
    $pdo = new PDO($dsn, $user, $pass);
    echo 'Połączenie OK';
} catch (PDOException $e) {
    echo 'Błąd połączenia: ' . $e->getMessage();
}

Host db wskazuje bezpośrednio na kontener z MySQL, bo Docker Compose tworzy dla wszystkich usług wspólną, prywatną sieć i automatycznie rozwiązuje nazwy.

Dodanie Nginx i przejście na PHP-FPM

W wielu projektach spotyka się układ Nginx + PHP-FPM. Nginx obsługuje HTTP, a PHP wykonuje skrypty w osobnym procesie. Taki schemat możesz zbudować także w Compose. Zamiast php:8.2-apache użyjemy obrazu z FPM:

version: "3.9"

services:
  app:
    image: php:8.2-fpm
    container_name: php-fpm-app
    volumes:
      - ./:/var/www/html

  web:
    image: nginx:1.27-alpine
    container_name: php-fpm-web
    ports:
      - "8080:80"
    volumes:
      - ./:/var/www/html
      - ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
    depends_on:
      - app

  db:
    image: mysql:8.0
    container_name: php-fpm-db
    environment:
      - MYSQL_ROOT_PASSWORD=root
      - MYSQL_DATABASE=app
      - MYSQL_USER=app
      - MYSQL_PASSWORD=app
    volumes:
      - db_data:/var/lib/mysql

volumes:
  db_data:

Potrzebny będzie jeszcze plik konfiguracyjny Nginx, np. docker/nginx/default.conf:

server {
    listen 80;
    server_name localhost;

    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 app:9000;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_param PATH_INFO $fastcgi_path_info;
    }
}

Kluczowy fragment to fastcgi_pass app:9000; – Nginx przekazuje żądania PHP do usługi app (kontener z PHP-FPM), port 9000. W praktyce taki zestaw jest bliższy produkcyjnemu środowisku, które spotkasz na wielu serwerach hostingowych i VPS-ach.

Praca developerska z logami i powłoką w kontenerze

Compose ułatwia podgląd logów konkretnej usługi. Kiedy coś nie działa, szybko sprawdzisz, co się dzieje w danym kontenerze:

docker compose logs web
docker compose logs app
docker compose logs db

Aby wejść do kontenera z PHP i coś sprawdzić „od środka”, użyj:

docker compose exec app bash

Po tej komendzie lądujesz w powłoce Linuksa wewnątrz kontenera, możesz uruchamiać php artisan, composer czy inne narzędzia tak, jak na zwykłym serwerze.

Przeniesienie istniejącego projektu PHP do Dockera

Od czego zacząć migrację starego projektu

Gotowe aplikacje, szczególnie starsze, często mają twardo zakodowane założenia o środowisku: ścieżki katalogów, wersję PHP, sposób instalacji rozszerzeń czy ręcznie konfigurowany Apache. Przenosząc taki projekt do Dockera, sensownie jest podzielić pracę na krótkie kroki:

  1. Ustalenie wersji PHP i wymaganych rozszerzeń,
  2. Przygotowanie minimalnego Dockerfile dla aplikacji,
  3. Dodanie bazy danych i innych usług poprzez Compose,
  4. Uporządkowanie konfiguracji (np. plików .env),
  5. Przeniesienie skryptów pomocniczych (cron, kolejki) do osobnych usług.

Taki podział sprawia, że nie trzeba wszystkiego przenosić „na raz”, co zwykle kończy się niekończącym się debugowaniem.

Analiza wymagań: wersja PHP, rozszerzenia, narzędzia

Pierwszym krokiem jest spisanie tego, czego aplikacja wymaga od środowiska. Dobrym punktem startu są:

  • pliki composer.json – sekcja require,
  • dotychczasowa dokumentacja projektu (jeśli istnieje),
  • konfiguracja serwera (np. stare pliki vhost Apache, php.ini),
  • gotowe instrukcje używane do stawiania środowiska na nowych maszynach.

Na tej podstawie dobierasz odpowiedni obraz bazowy, np. php:7.4-apache dla legacy aplikacji czy php:8.2-fpm z Nginx dla nowszych projektów. Jeżeli rozszerzenia typu pdo_mysql, gd, intl są wymagane, dopisujesz je w Dockerfile przez docker-php-ext-install.

Tworzenie Dockerfile pod istniejącą aplikację

Zakładając, że projekt to np. aplikacja w Laravelu z PHP-FPM, prosty Dockerfile może wyglądać tak:

FROM php:8.2-fpm

# Instalacja pakietów systemowych potrzebnych do rozszerzeń
RUN apt-get update && apt-get install -y 
        git 
        unzip 
        libzip-dev 
        libicu-dev 
    && docker-php-ext-install pdo_mysql intl zip 
    && rm -rf /var/lib/apt/lists/*

# Instalacja Composera
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer

# Ustaw katalog roboczy
WORKDIR /var/www/html

# Kopiuj pliki projektu (w produkcji zwykle z .dockerignore)
COPY . .

# Instalacja zależności PHP
RUN composer install --no-dev --optimize-autoloader

# Opcjonalne cache configu, routingu itp. (np. dla Laravel)
# RUN php artisan config:cache && php artisan route:cache

Taki obraz możesz zintegrować z Compose, zastępując sekcję image: definicją budowania:

services:
  app:
    build:
      context: .
      dockerfile: Dockerfile
    volumes:
      - ./:/var/www/html
    # ...

W środowisku developerskim często instalacja zależności w trakcie budowania obrazu nie jest konieczna, jeśli zależy ci na szybkim odświeżaniu. Wtedy można uruchamiać composer install w kontenerze podczas pracy.

Dostosowanie konfiguracji bazy danych i innych usług

Większość istniejących projektów trzyma konfigurację w plikach typu .env. Przy przejściu na Dockera zmienia się głównie sposób adresowania usług. localhost przestaje być adresem bazy; zamiast tego korzystasz z nazwy serwisu Compose.

Dla Laravela typowy fragment .env mógłby wyglądać tak:

DB_CONNECTION=mysql
DB_HOST=db
DB_PORT=3306
DB_DATABASE=app
DB_USERNAME=app
DB_PASSWORD=app

Podobnie ustawisz Redis, RabbitMQ czy inne usługi. Ważne, aby nazwa hosta pokrywała się z nazwą usługi w docker-compose.yml.

Migracja crona i zadań cyklicznych

Starsze projekty często korzystają z crona na serwerze, np. do wysyłki newsletterów czy sprzątania logów. W świecie Dockera wygodniej jest potraktować takie zadania jako osobną usługę. Przykładowo, dla aplikacji w Laravelu można dodać serwis:

services:
  scheduler:
    image: php:8.2-cli
    working_dir: /var/www/html
    volumes:
      - ./:/var/www/html
    depends_on:
      - app
    command: bash -c "while true; do php artisan schedule:run; sleep 60; done"

To proste podejście, które w środowisku developerskim jest zupełnie wystarczające. W produkcji można użyć dedykowanych narzędzi do harmonogramowania, ale struktura Compose pozostaje podobna.

Stopniowe odcinanie się od „lokalnych” zależności

Przenosząc projekt do Dockera, często okazuje się, że aplikacja polega na programach zainstalowanych bezpośrednio w systemie: np. narzędziach konwertujących obrazy, binarkach do generowania PDF-ów czy skryptach w Pythonie. Z czasem przenosząc te zależności do obrazu dockera albo osobnych usług, zyskujesz pełną powtarzalność środowiska.

Przykład: jeśli aplikacja wywołuje program wkhtmltopdf, możesz doinstalować go w Dockerfile do obrazu PHP, zamiast instalować lokalnie na każdym komputerze developera. Po kilku takich rundach zyskujesz środowisko, które startuje identycznie na każdej maszynie, bez długich instrukcji „co zainstalować”.

Optymalna struktura projektu PHP pod Dockera (foldery, konfiguracje, .env)

Uporządkowany układ katalogów dla typowego projektu

Gdy Docker wchodzi na stałe do projektu, dobrze jest od razu uporządkować strukturę katalogów. Intuicyjny układ przydaje się zwłaszcza w większych zespołach. Przykładowa struktura może wyglądać tak:

projekt-php/
├── src/                # kod aplikacji (np. app/, public/, vendor/ itp.)
├── docker/
│   ├── nginx/
│   │   └── default.conf
│   ├── php/
│   │   └── Dockerfile
│   └── mysql/
│       └── my.cnf      # opcjonalna konfiguracja MySQL
├── .env                # konfiguracja aplikacji (nie Dockera!)
├── .env.example
├── docker-compose.yml
├── .dockerignore
└── README.md

Kod aplikacji trzymasz w jednym miejscu (tu: src/), a wszystko związane z Dockerem w katalogu docker/ oraz pliku docker-compose.yml na poziomie głównym. Dzięki temu od razu widać, co jest infrastrukturą, a co domeną biznesową projektu.

Rozdzielenie konfiguracji aplikacji i Dockera

Częste nieporozumienie polega na mieszaniu .env aplikacji z ustawieniami Dockera. Lepiej traktować to osobno:

.env aplikacji przechowuje dane, które sama aplikacja odczytuje przez funkcje typu getenv() czy mechanizm konfiguracji frameworka (Laravel, Symfony). Natomiast środowisko Dockera najlepiej opisać w osobnym pliku, np. .env.docker lub .env.local, dołączanym w docker-compose.yml przez dyrektywę env_file. Dzięki temu zmiana hasła do bazy w środowisku developerskim nie wymusza modyfikacji plików konfiguracyjnych aplikacji trzymanych w repozytorium.

Przykładowo, możesz mieć w katalogu głównym dwa pliki: .env (z konfiguracją domeny aplikacji, np. tryb debugowania, klucze API, konfiguracja logów) oraz .env.docker (z nazwami kontenerów, portami wystawionymi na hosta, nazwami sieci). W docker-compose.yml dodajesz wtedy:

services:
  app:
    env_file:
      - .env
      - .env.docker

Dzięki takiemu podziałowi możesz np. używać tych samych zmiennych aplikacji na serwerze stagingowym, ale zupełnie innej konfiguracji Dockera niż lokalnie. Unikasz też sytuacji, w której parametry typowo „serwerowe” wlewają się do plików konfiguracyjnych, które miał obsługiwać wyłącznie framework.

.env, .env.example i .dockerignore – małe pliki, duży porządek

Plik .env.example pełni rolę szablonu konfiguracji. Trzymasz w nim klucze (nazwy zmiennych), ale bez wrażliwych wartości. Nowa osoba w projekcie kopiuje go do .env i uzupełnia brakujące dane. W projektach dockerowych dobrze jest od razu dodać do niego sekcję z parametrami typowymi dla Dockera: domyślną nazwą bazy, użytkownika, portami itp. To jest pierwsze miejsce, w które zajrzy ktoś stawiający środowisko po raz pierwszy.

.dockerignore decyduje, które pliki nie powinny trafiać do obrazu podczas budowania. Typowa zawartość to katalogi vendor/, node_modules/, pliki logów, cache i inne śmieci generowane lokalnie. Dzięki temu build jest szybszy, a obraz mniejszy. Dla obrazów produkcyjnych często wyklucza się też .env, żeby nie pakować poufnych danych bezpośrednio do obrazu – lepiej przekazać je jako zmienne środowiskowe na etapie uruchamiania kontenera.

Profile środowisk: development, staging, produkcja

Ta sama aplikacja PHP zazwyczaj żyje w co najmniej trzech „światach”: na komputerach developerów, na serwerze testowym oraz w produkcji. Docker ułatwia ujednolicenie tych światów, ale nie wszystko powinno wyglądać identycznie. Rozsądne jest wydzielenie profili środowiskowych.

Jednym z prostszych sposobów jest trzymanie kilku plików Compose:

docker-compose.yml          # podstawowa definicja usług
docker-compose.override.yml # lokalne różnice (dev)
docker-compose.prod.yml     # produkcyjne nadpisania

Podstawowy plik zawiera wspólny „rdzeń”:

version: "3.9"

services:
  app:
    build:
      context: .
      dockerfile: docker/php/Dockerfile
    env_file:
      - .env
    volumes:
      - ./src:/var/www/html
    depends_on:
      - db

  db:
    image: mysql:8.0
    environment:
      MYSQL_DATABASE: ${DB_DATABASE}
      MYSQL_USER: ${DB_USERNAME}
      MYSQL_PASSWORD: ${DB_PASSWORD}
      MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}

Plik docker-compose.override.yml jest automatycznie ładowany lokalnie, więc można w nim włączyć np. wystawienie portów i tryb debugowania:

services:
  app:
    ports:
      - "8080:80"
    environment:
      APP_ENV: local
      APP_DEBUG: 1
    volumes:
      - ./docker/php/custom.ini:/usr/local/etc/php/conf.d/custom.ini

Konfigurację produkcyjną uruchamiasz dodatkowymi plikami:

docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d

W takim pliku możesz np. podmienić obraz na zbudowany w CI, wyłączyć mountowanie wolumenów z kodem (tylko gotowy build) i przełączyć logowanie.

Konfiguracja Xdebug i debugowanie w kontenerze

Praca z Dockerem nie oznacza końca wygodnego debugowania w IDE. Xdebug radzi sobie z kontenerami całkiem dobrze, jeśli poprawnie ustawisz kilka zmiennych.

Dla obrazu PHP 8.2-FPM instalacja Xdebuga w Dockerfile może wyglądać tak:

RUN pecl install xdebug 
    && docker-php-ext-enable xdebug

Następnie dodajesz plik konfiguracyjny, np. docker/php/xdebug.ini:

[xdebug]
xdebug.mode=develop,debug
xdebug.start_with_request=yes
xdebug.client_host=host.docker.internal
xdebug.client_port=9003

W docker-compose.override.yml montujesz ten plik oraz wystawiasz port:

services:
  app:
    volumes:
      - ./src:/var/www/html
      - ./docker/php/xdebug.ini:/usr/local/etc/php/conf.d/xdebug.ini
    ports:
      - "8080:80"
      - "9003:9003"

Adres host.docker.internal to wygodna „skrótowa” nazwa hosta widzianego z kontenera w systemach typu Docker Desktop. Na Linuksie czasem trzeba go zastąpić adresem IP hosta lub użyć sieci typu host. W IDE (np. PHPStorm, VS Code z odpowiednią wtyczką) ustawiasz nasłuchiwanie na porcie 9003 i mapowanie ścieżek: katalog projektu na hoście na /var/www/html w kontenerze.

Logi i persistent data – gdzie lądują dane w świecie kontenerów

Kontener jest „ulotny” – po usunięciu cała jego zawartość znika. Dla logów i danych aplikacji trzeba mieć jasno ustalone miejsce, które przetrwa restart. Najprostsza zasada: kod i konfiguracje wersjonujesz w Git, dane i logi trzymasz w wolumenach lub katalogach na hoście.

W docker-compose.yml możesz zadeklarować nazwane wolumeny:

services:
  db:
    image: mysql:8.0
    volumes:
      - db_data:/var/lib/mysql

  app:
    image: php:8.2-fpm
    volumes:
      - ./src:/var/www/html
      - logs_app:/var/www/html/storage/logs

volumes:
  db_data:
  logs_app:

Dzięki temu logi i baza nie znikną po docker compose down. W środowisku lokalnym część osób woli montować logi na hosta, aby łatwo je podglądać:

services:
  app:
    volumes:
      - ./src:/var/www/html
      - ./logs/app:/var/www/html/storage/logs

Ważne jest też, aby aplikacja tworzyła logi w jednym przewidywalnym miejscu. W Laravelu czy Symfony to zwykle katalog storage/logs lub var/log, co ułatwia wystandaryzowanie konfiguracji we wszystkich środowiskach.

Budowanie obrazów produkcyjnych: wieloetapowe Dockerfile

Na laptopie liczy się szybkość budowania i elastyczność. W produkcji priorytetem staje się bezpieczeństwo i lekki obraz. Do tego służy wieloetapowe buildy (multi-stage), w których w pierwszym etapie kompilujesz zależności, a w drugim zostawiasz tylko to, co potrzebne do uruchomienia.

Przykładowy Dockerfile dla aplikacji w Laravelu może wyglądać tak:

FROM composer:2 AS vendor

WORKDIR /app
COPY src/composer.json src/composer.lock ./
RUN composer install --no-dev --no-scripts --no-progress --prefer-dist

FROM node:18 AS assets

WORKDIR /app
COPY src/package.json src/package-lock.json ./
RUN npm install
COPY src/resources ./resources
RUN npm run build

FROM php:8.2-fpm AS app

RUN apt-get update && apt-get install -y 
        libzip-dev 
        libicu-dev 
    && docker-php-ext-install pdo_mysql intl zip 
    && rm -rf /var/lib/apt/lists/*

WORKDIR /var/www/html

COPY src ./
COPY --from=vendor /app/vendor ./vendor
COPY --from=assets /app/public/build ./public/build

RUN php artisan config:cache 
    && php artisan route:cache

CMD ["php-fpm"]

W efekcie końcowy obraz zawiera tylko PHP-FPM z Twoją aplikacją, zależnościami i zbudowanymi assetami frontendu. Etap z Node i Composerem nie trafia do produkcji, dzięki czemu obraz jest mniejszy, a powierzchnia ataku – mniejsza.

W tym miejscu przyda się jeszcze jeden praktyczny punkt odniesienia: Jak mikroserwisy wpływają na wydajność aplikacji.

Cache’owanie zależności i szybkie buildy

Docker stara się cache’ować każdy krok budowania obrazu. Kolejność instrukcji w Dockerfile ma więc duże znaczenie. Intuicja jest prosta: na górze trzymaj rzeczy, które zmieniają się rzadko (system, rozszerzenia PHP), a niżej te, które zmieniają się często (kod aplikacji).

Dla PHP z Composerem dobrym wzorcem jest osobne kopiowanie plików composer.json i composer.lock przed właściwym kodem:

WORKDIR /var/www/html

COPY src/composer.json src/composer.lock ./
RUN composer install --no-dev --no-progress --prefer-dist

COPY src ./

Dopóki nie zmienisz plików Composera, Docker wykorzysta stary cache composer install, co przyspiesza buildy nawet kilkukrotnie. Zmiana pojedynczego kontrolera czy widoku nie będzie uruchamiać ponownej instalacji zależności.

Bezpieczeństwo w kontenerach PHP – kilka podstawowych nawyków

Docker nie jest magiczną tarczą ochronną. Kod PHP dalej może mieć podatności, a źle skonfigurowany obraz ułatwi atak. Kilka prostych praktyk znacznie poprawia sytuację.

  • Uruchamianie aplikacji bez roota. Domyślnie procesy w kontenerze działają jako root. Warto dodać w Dockerfile użytkownika o ograniczonych uprawnieniach:
    RUN useradd -u 1000 -m appuser
    USER appuser

    i upewnić się, że katalogi aplikacji mają odpowiednie prawa.

  • Ograniczenie ekspozycji portów. W produkcji nie trzeba wystawiać portu PHP-FPM na świat. Wystarczy, że Nginx widzi usługę po wewnętrznej sieci Dockera. W docker-compose.yml porty wystawiasz raczej tylko dla frontowego serwera HTTP.
  • Brak narzędzi developerskich w produkcji. Xdebug, narzędzia benchmarkowe, testowe skrypty – to wszystko powinno zostać w obrazach developerskich. W obrazach produkcyjnych można je wyłączyć osobnym plikiem Dockerfile lub dodatkowymi etapami builda.
  • Aktualizacje obrazów bazowych. Jeśli korzystasz z obrazów php:8.x-fpm, dobrze ustawić cykliczne rebuildy, żeby wciągać aktualizacje bezpieczeństwa. System pakietów (apt, apk) nie aktualizuje się sam – trzeba zbudować obraz ponownie.

Praca zespołowa: ustandaryzowane komendy i Makefile

Im bardziej rośnie projekt, tym częściej powtarzają się te same operacje: uruchomienie kontenerów, wykonanie migracji, odpalanie testów. Zamiast wymieniać się w zespole pełnymi komendami Dockera, wygodniej jest ukryć je pod prostymi skrótami.

Najprostszym narzędziem do tego jest Makefile w katalogu głównym:

up:
    docker compose up -d

down:
    docker compose down

logs:
    docker compose logs -f app

bash:
    docker compose exec app bash

migrate:
    docker compose exec app php artisan migrate

test:
    docker compose exec app php artisan test

Dzięki temu nowa osoba w projekcie nie musi uczyć się całego zestawu komend Dockera. Wystarczy, że wywoła make up, make bash, make migrate. Dodatkowy plus: dokumentujesz w ten sposób typowy workflow pracy z projektem.

Testy jednostkowe i integracyjne w kontenerach

Docker świetnie nadaje się do uruchamiania testów w identycznym środowisku za każdym razem. W praktyce stosuje się dwa podejścia: odpalanie testów w tym samym kontenerze, w którym działa aplikacja, albo dedykowany serwis testowy w Compose.

W prostszym wariancie dodajesz do istniejącego serwisu polecenie testów:

docker compose exec app php vendor/bin/phpunit

W bardziej rozbudowanych projektach bywa wygodne wydzielenie osobnej usługi test w docker-compose.yml:

services:
  test:
    build:
      context: .
      dockerfile: docker/php/Dockerfile
    command: php vendor/bin/phpunit
    depends_on:
      - db
    volumes:
      - ./src:/var/www/html

Taki serwis można uruchamiać jednorazowo w CI/CD lub lokalnie, nie dotykając kontenerów developerskich. Przy testach integracyjnych z bazą danych przydaje się osobna baza testowa, np. DB_DATABASE=app_test, aby nie mieszać rekordów z danych testowych z danymi developerskimi.

Import i eksport danych – migawki bazy w świecie Dockera

Przy pracy nad legacy aplikacjami PHP często pojawia się potrzeba wczytania kopii bazy danych z produkcji lub podzielenia się „paczką” danych z innym członkiem zespołu. Z kontenerami można to ogarnąć w kilku prostych poleceniach.

Jeśli baza działa w usługach Compose jako db, zrzut zrobisz w ten sposób:

docker compose exec db 
  sh -c 'mysqldump -u$$MYSQL_USER -p$$MYSQL_PASSWORD $$MYSQL_DATABASE' 
  > dump.sql

Import wygląda symetrycznie:

docker compose exec -T db 
  sh -c 'mysql -u$$MYSQL_USER -p$$MYSQL_PASSWORD $$MYSQL_DATABASE' 
  < dump.sql

Opcja -T wyłącza pseudo-tty, dzięki czemu można bez problemu przekierować plik. W praktyce wystarczy, że jedna osoba w zespole utworzy taki zrzut i wrzuci w bezpieczne miejsce (np. zaszyfrowane archiwum), reszta zaimportuje go jednym poleceniem.

Równoległe wersje PHP – kilka stosów w jednym projekcie

Niektóre zespoły muszą utrzymywać kilka aplikacji w różnych wersjach PHP, czasem nawet w jednym repozytorium. Na serwerze fizycznym to spore wyzwanie, natomiast w Dockerze wystarczy kilka usług z różnymi obrazami i portami.

Przykład: dwa serwisy app74 i app82 w docker-compose.yml:

services:
  app74:
    image: php:7.4-apache
    volumes:
      - ./src-legacy:/var/www/html
    ports:
      - "8074:80"

  app82:
    image: php:8.2-apache
    volumes:
      - ./src:/var/www/html
    ports:
      - "8082:80"

Można je rozszerzyć o wspólną bazę danych albo osobne instancje, w zależności od potrzeb. To przydatne np. przy migracji starego systemu do nowszego frameworka: oba środowiska są zawsze pod ręką, w pełni odseparowane i bez wzajemnego konfliktu konfiguracji PHP.

Debugowanie aplikacji PHP w kontenerach

Środowisko w Dockerze nie powinno utrudniać szukania błędów. Cel jest odwrotny: szybciej dojść do sedna, niezależnie od tego, na jakim komputerze działa projekt.

Logi PHP i serwera – gdzie ich szukać

Jeżeli aplikacja nagle zaczyna wyrzucać białą stronę lub 500, pierwszym krokiem są logi. W środowisku kontenerowym zazwyczaj interesują Cię trzy źródła:

  • logi serwera HTTP (Apache/Nginx),
  • logi PHP-FPM lub PHP w Apache,
  • logi samej aplikacji (np. storage/logs w Laravelu).

Podstawowy trik: zamiast szukać plików logów w kontenerze, często wystarczy samo:

docker compose logs -f app

Jeżeli w kontenerze logi trafiają do stderr/stdout, pojawią się od razu w konsoli. Gdy aplikacja trzyma własne pliki logów w wolumenie, możesz je przeglądać bezpośrednio z hosta, np.:

tail -f src/storage/logs/laravel.log

Xdebug w Dockerze – jak nie zwariować z siecią

Xdebug w kontenerach bywa postrachem, bo pojawia się temat adresów IP i portów. Da się to jednak sprowadzić do kilku prostych kroków.

Najpierw użyj dedykowanego obrazu lub doinstaluj Xdebug w swoim Dockerfile developerskim:

RUN pecl install xdebug 
    && docker-php-ext-enable xdebug

Następnie skonfiguruj go przez xdebug.ini (albo w php.ini):

xdebug.mode=debug
xdebug.start_with_request=yes
xdebug.client_port=9003
xdebug.discover_client_host=true

Opcja xdebug.discover_client_host często wystarcza: Xdebug sam spróbuje znaleźć adres IP hosta, z którego przyszło żądanie. W bardziej złożonych przypadkach możesz ustawić adres ręcznie, np. w Docker Desktop na Windows/Mac:

xdebug.client_host=host.docker.internal

W IDE (PHPStorm, VS Code z odpowiednią wtyczką) ustaw ten sam port, co w Xdebug (9003 dla Xdebug 3), włącz nasłuchiwanie i ustaw breakpointy. Po wejściu na stronę w przeglądarce debuger powinien „złapać” wykonanie kodu tak samo, jak przy lokalnym PHP.

Debugowanie CLI i zadań w tle

Kontenery świetnie nadają się do uruchamiania komend CLI i workerów kolejek. Aby debugować takie procesy, uruchamiaj je przez docker compose exec:

docker compose exec app php artisan queue:work

Jeżeli Xdebug jest aktywny, te procesy również mogą wywołać sesję debugowania w IDE (przy włączonym nasłuchiwaniu). W praktyce wygodne bywa stworzenie osobnej konfiguracji, w której Xdebug jest włączony tylko dla CLI, a dla HTTP – nie, aby nie spowalniać wszystkich żądań przeglądarki.

Programista w czarnej bluzie pracuje nad kodem PHP na laptopie
Źródło: Pexels | Autor: Sora Shimazaki

Środowisko deweloperskie vs produkcyjne – dwa różne światy

Docker sprzyja temu, żeby „wszystko było w jednym pliku”, ale dobre rozdzielenie środowisk odwdzięcza się spokojniejszymi wdrożeniami.

Oddzielne pliki Compose dla dev i prod

Zdrowy nawyk to utrzymywanie dwóch (lub więcej) plików Compose: bazowego i rozszerzającego go dla konkretnego środowiska. Przykład prostego układu:

  • docker-compose.yml – definicja podstawowych usług, wspólna dla wszystkich,
  • docker-compose.dev.yml – rzeczy tylko dla deweloperów: Xdebug, wolumeny z kodem, dodatkowe narzędzia,
  • docker-compose.prod.yml – np. inne limity zasobów, brak mapowania kodu z hosta, gotowy obraz z CI.

Uruchamiasz środowisko dev tak:

docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d

a produkcyjne:

docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d

W praktyce dzięki temu w jednym repo trzymasz deklarację całej infrastruktury, a jednocześnie nie przenosisz przypadkiem Xdebuga ani zbędnych portów do produkcji.

Różne konfiguracje PHP za pomocą ini i env

Konfiguracja PHP dla dev i prod z definicji powinna się różnić: w dev – display_errors=On, niski poziom cache, w prod – odwrotnie. Zamiast tworzyć dwa zupełnie różne obrazy, można korzystać z osobnych plików ini montowanych jako wolumeny:

Do kompletu polecam jeszcze: Jak programować w Solidity – pierwsze kroki w tworzeniu smart kontraktów — znajdziesz tam dodatkowe wskazówki.

services:
  app:
    build: .
    volumes:
      - ./docker/php/dev.ini:/usr/local/etc/php/conf.d/app.ini

W produkcji ten wolumen zastępujesz innym:

services:
  app:
    image: registry.example.com/php-app:latest
    volumes:
      - ./docker/php/prod.ini:/usr/local/etc/php/conf.d/app.ini

Poziom logowania czy tryb buforowania opcji OPCache możesz dodatkowo spinać z .env, korzystając z funkcji środowiskowych w konfiguracji frameworka (np. w Laravelu – config/cache.php, config/app.php).

Build raz, uruchamiaj wszędzie

Przy większych zespołach sensowne jest podejście: obraz aplikacji budowany jest tylko w CI, a programiści i serwery produkcyjne po prostu go pobierają z rejestru. Dzięki temu zawartość obrazu jest identyczna na każdym etapie.

Przykładowy fragment pliku Compose używający gotowego obrazu:

services:
  app:
    image: registry.example.com/team/app-php:1.2.3
    environment:
      - APP_ENV=production
      - APP_DEBUG=false

Na środowisku testowym możesz podmienić tylko zmienne środowiskowe i porty, a kod aplikacji w obrazie zostaje ten sam. To bardzo upraszcza analizę błędów: jeśli coś działa w stagingu, a nie działa w produkcji, szukasz różnicy w konfiguracji, nie w samym obrazie.

Docker a CI/CD dla projektów PHP

Konteneryzacja naturalnie łączy się z automatycznymi pipeline’ami: build, testy, deploy. Nawet prosty skrypt w GitLab CI czy GitHub Actions może zrobić dużą różnicę w przewidywalności wdrożeń.

Minimalny pipeline dla aplikacji PHP z Dockerem

Typowy scenariusz obejmuje trzy kroki: zbudowanie obrazu, odpalenie testów w tym obrazie i ewentualnie wypchnięcie go do rejestru. Przykład prostego pipeline’u w GitLab CI:

stages:
  - build
  - test
  - deploy

build:
  stage: build
  script:
    - docker build -t registry.example.com/app-php:${CI_COMMIT_SHA} .
    - docker push registry.example.com/app-php:${CI_COMMIT_SHA}

test:
  stage: test
  script:
    - docker run --rm registry.example.com/app-php:${CI_COMMIT_SHA} 
        php vendor/bin/phpunit

Dopiero gdy testy przejdą, obraz trafia do etapu deploy. Serwer produkcyjny może dostać konkretną wersję obrazu po SHA commita lub oznaczoną tagiem, np. 1.5.0.

Testy integracyjne z bazą i usługami towarzyszącymi

Największy zysk z CI pojawia się wtedy, gdy testy uruchamiane są w takim samym stosie usług, jak lokalnie. Docker Compose świetnie do tego pasuje: można go wprost użyć w pipeline’ie.

Fragment konfiguracji dla GitHub Actions może wyglądać następująco:

- name: Uruchom usługi testowe
  run: docker compose -f docker-compose.yml -f docker-compose.test.yml up -d

- name: PHPUnit
  run: docker compose exec -T app php vendor/bin/phpunit

W takim wariancie testy widzą prawdziwą bazę MySQL czy Redis, a nie sztuczne stuby. Jeśli potem uruchamiasz tę samą komendę lokalnie, błędy zazwyczaj da się łatwo odtworzyć.

Workflows dla webmastera – praktyczne scenariusze użycia Dockera z PHP

Sam Docker to tylko narzędzie. Największa zmiana przychodzi z ułożeniem codziennych nawyków wokół kontenerów.

Szybkie „piaskownice” dla obcych projektów

Gdy dostajesz stary projekt klienta na FTP lub zipie, nie musisz już instalować lokalnego PHP 7.3, MySQL 5.7 i szukać właściwej wersji IonCube. Tworzysz mały plik Compose tylko dla tego projektu, np.:

services:
  web:
    image: php:7.3-apache
    volumes:
      - ./projekt-klienta:/var/www/html
    ports:
      - "8073:80"

Po docker compose up -d masz odseparowaną piaskownicę, której nie mieszasz z głównym środowiskiem pracy. Po zakończeniu analizy robisz docker compose down i nic nie zostaje w systemie poza katalogiem z plikami klienta.

Równoległa praca nad wieloma projektami

Większość webmasterów obsługuje kilka, kilkanaście stron równolegle. Docker pozwala trzymać każdą w osobnym zestawie kontenerów, z własnymi portami, wersjami PHP i bazą danych. W praktyce możesz mieć jednocześnie:

  • projekt A na http://localhost:8081 z PHP 8.2 i MySQL 8,
  • projekt B na http://localhost:8082 z PHP 7.4 i MariaDB,
  • projekt C – statyczną stronę z małym API w PHP 8.1.

System operacyjny widzi tylko kilka procesów Dockera, a cała złożoność siedzi wewnątrz kontenerów. Znika problem „zabiłem Apache’a od innego projektu” albo „ta wersja rozszerzenia PHP wywaliła mi resztę stron”.

Eksperymenty z nowymi wersjami PHP i frameworków

Przed aktualizacją produkcji sensownie jest „przeklikać” projekt na nowszej wersji PHP czy frameworka. Zamiast instalować ręcznie PHP 8.3 na serwerze testowym, dopisujesz równoległą usługę w Compose:

services:
  app_old:
    image: php:8.0-apache
    volumes:
      - ./src:/var/www/html
    ports:
      - "8080:80"

  app_new:
    image: php:8.3-apache
    volumes:
      - ./src:/var/www/html
    ports:
      - "8083:80"

Możesz przechodzić między adresami i sprawdzać, co się sypie przy nowej wersji, bez ingerencji w system operacyjny. Jeśli aktualizacja wymaga innej wersji rozszerzenia czy dodatkowej biblioteki, wprowadzasz ją tylko w jednym serwisie.

Konfiguracja za pomocą zmiennych środowiskowych i sekretów

Pliki konfiguracyjne w PHP często lądują w repozytorium. W kontenerach dużo wygodniej przekazywać dane konfiguracyjne przez zmienne środowiskowe, a wrażliwe hasła poprzez dedykowane mechanizmy.

Zmienna środowiskowa jako źródło ustawień

Większość współczesnych frameworków PHP (Laravel, Symfony, Slim) świetnie dogaduje się z .env. W Dockerze możesz traktować ten plik jako szablon, a konkretne wartości przekazywać już jako zmienne środowiskowe:

services:
  app:
    image: registry.example.com/app-php:latest
    environment:
      APP_ENV: production
      APP_DEBUG: "false"
      DB_HOST: db
      DB_DATABASE: app
      DB_USERNAME: app
      DB_PASSWORD: super-haslo

W kodzie PHP te wartości będą dostępne tak samo jak z lokalnego pliku .env. Różnica polega na tym, że w repozytorium nie trzymasz prawdziwych haseł, tylko ich „sloty” (np. DB_PASSWORD=<ustaw w CI>).

Sekrety i hasła poza repozytorium

Hasła do baz, klucze API czy tokeny logowania do zewnętrznych usług lepiej trzymać poza Git-em. Prosty wariant to korzystanie z funkcji secrets w Docker Swarm albo z menedżera sekretów w chmurze (np. AWS Secrets Manager).

Jeżeli nie używasz klastra, a jedynie pojedynczego serwera z Compose, minimalnym zabezpieczeniem jest trzymanie pliku z prawdziwymi danymi poza repozytorium (np. .env.prod tylko na serwerze) i ładowanie go bezpośrednio w Compose:

services:
  app:
    env_file:
      - .env.prod

Repozytorium zawiera wtedy jedynie .env.example z pustymi polami, które nowe osoby w zespole wypełniają lokalnie według instrukcji.

Monitorowanie i analiza wydajności aplikacji PHP w kontenerach

Kontenery pomagają uruchomić aplikację, ale równie ważne jest obserwowanie, jak się zachowuje pod obciążeniem. Nawet w małych projektach przydaje się podgląd podstawowych metryk.

Statystyki Dockera jako pierwszy krok

Bez instalowania dodatkowych narzędzi masz pod ręką prostą komendę:

docker stats

Pokazuje użycie CPU, pamięci i sieci dla każdego kontenera. Gdy strona zaczyna reagować powoli, a kontener z PHP nagle zużywa kilka razy więcej RAM-u niż zwykle, to pierwszy trop. Pozwala też złapać „uciekający” proces CLI, który zapomniałeś wyłączyć.

Profilowanie PHP w środowisku kontenerowym

Dla głębszej analizy działania kodu można użyć narzędzi takich jak XHProf, Tideways czy Blackfire. W wariancie „na szybko” często wystarczy osobna konfiguracja z Xdebug w trybie profiler:

xdebug.mode=profile
xdebug.output_dir=/tmp/xdebug

W kontenerze ustawiasz dodatkowy wolumen na katalog z profilami:

„`html

services:
  php:
    build:
      context: .
    volumes:
      - ./src:/var/www/html
      - ./profiling:/tmp/xdebug

Każde wejście na stronę generuje plik profilu w katalogu ./profiling na hoście. Potem można go otworzyć w narzędziach takich jak QCacheGrind lub Webgrind i prześledzić, które funkcje zabierają najwięcej czasu. Kluczowe jest, żeby taką konfigurację traktować jak tryb diagnostyczny, a nie ustawienie produkcyjne.

Profilowanie dobrze sprawdza się w duecie z prostym testem obciążeniowym. Nawet kilkanaście równoległych żądań z narzędzia typu ab lub wrk potrafi ujawnić „wąskie gardła” – od zapytań SQL po niepotrzebne pętle w szablonach. Gdy test odpalasz na kontenerze, bardzo łatwo odtworzyć warunki: ta sama wersja PHP, te same rozszerzenia, identyczna konfiguracja.

Jeżeli aplikacja rośnie, dobrze jest dorzucić lekki monitoring logów i metryk. Popularny zestaw to kontener z Prometheusem i Grafaną oraz osobny kontener do zbierania logów (np. Fluent Bit). Nawet kilka podstawowych wykresów – użycie CPU, RAM, liczba żądań na sekundę, czas odpowiedzi – wystarczy, żeby szybciej reagować, zamiast „gasić pożary” dopiero po skargach użytkowników.

Docker porządkuje środowisko, ale też wymusza kilka zdrowych nawyków: czytelne zależności, wersjonowanie konfiguracji, automatyczne testy, powtarzalne wdrożenia. Przy projektach PHP oznacza to mniej niespodzianek przy aktualizacjach, łatwiejszą współpracę z innymi i spokojniejszą głowę, gdy trzeba szybko przenieść stronę na nowy serwer lub podnieść wersję całego stosu.

Najczęściej zadawane pytania (FAQ)

Czym Docker różni się od wirtualnej maszyny przy pracy z PHP?

Wirtualna maszyna uruchamia cały system w systemie: własne jądro, sterowniki, pełen zestaw usług. Docker korzysta z jądra hosta, ale izoluje aplikację, biblioteki i konfigurację w lekkim kontenerze. Efekt jest taki, że kontener startuje w sekundy, a nie minuty, i zużywa mniej RAM oraz CPU.

Dla webmastera oznacza to, że środowisko z PHP i serwerem WWW działa szybciej, da się go uruchomić w wielu kopiach równolegle i łatwo przenieść między komputerami. Zyskujesz wygodę „gotowego pakietu” (jak XAMPP) połączoną z elastycznością ręcznie ustawionego serwera, tylko w formie powtarzalnego obrazu Dockera.

Dlaczego używanie Dockera do projektów PHP ma sens dla webmastera?

Docker rozwiązuje typowe konflikty środowiskowe: różne wersje PHP, inne konfiguracje MySQL, brakujące rozszerzenia. Każdy projekt ma własny obraz z konkretną wersją PHP i własną bazą danych, więc aktualizacja jednego projektu nie rozbija drugiego.

Do tego dochodzi powtarzalność: ten sam Dockerfile i docker-compose.yml możesz uruchomić na Windowsie, macOS i Linuksie. W praktyce zmniejsza to liczbę sytuacji typu „u mnie działa”, bo cała ekipa uruchamia projekt w identycznym środowisku.

Czy Docker zastąpi mi XAMPP/LAMP przy lokalnym PHP?

Tak, Docker może w pełni zastąpić XAMPP czy ręcznie składany LAMP na laptopie. Zamiast jednego wspólnego serwera na całym systemie, masz osobne środowisko per projekt: osobne PHP, osobny serwer WWW, osobną bazę danych.

Dla przykładu: jeden kontener z PHP 7.4 i MySQL 5.7 może obsługiwać starego WordPressa, a drugi z PHP 8.2 i MySQL 8 – nowego Laravela. Oba działają równolegle, nie wchodząc sobie w drogę. System hosta pozostaje „czysty”, bo nie instalujesz na nim stosu LAMP.

Co to jest obraz (image) i kontener w Dockerze dla PHP?

Obraz Dockera to jak paczka ZIP z całym środowiskiem: system bazowy (np. Debian Slim), PHP, rozszerzenia i ustawienia. Ma tag, np. php:8.2-apache czy php:8.1-fpm-alpine, który określa wersję PHP i rodzaj serwera (Apache albo FPM pod Nginx). Instrukcje budowy obrazu zapisuje się w pliku Dockerfile.

Kontener to działający egzemplarz obrazu. Z jednego obrazu możesz uruchomić wiele kontenerów, każdy z własnymi procesami i systemem plików. W praktyce kontener to np. „serwer PHP-FPM dla projektu X”, który można zatrzymywać, kasować i odtwarzać z obrazu, a dane trzymać na wolumenach tak, by nie zginęły przy przeinstalowaniu kontenera.

Do czego służy Docker Compose w projektach PHP i czy muszę go używać?

Docker Compose pozwala opisać całe środowisko projektu w jednym pliku YAML – zwykle docker-compose.yml. W typowym projekcie PHP obejmuje to serwis z aplikacją (PHP-FPM lub PHP+Apache), serwis bazy danych (MySQL/MariaDB), czasem cache (Redis) i Nginx jako reverse proxy.

Teoretycznie możesz wszystko uruchamiać ręcznie komendami docker run, ale przy kilku kontenerach szybko robi się to uciążliwe. Docker Compose działa jak „skrypt startowy” dla całego projektu: jednym docker compose up stawiasz całe środowisko, a jednym docker compose down je wyłączasz, co przyspiesza codzienną pracę.

Jak Docker pomaga, gdy zespół pracuje na różnych systemach (Windows, macOS, Linux)?

Bez kontenerów każdy członek zespołu zwykle ma inną wersję PHP, inny php.ini i inną konfigurację serwera WWW. Błąd zgłoszony na jednym komputerze bywa trudny do odtworzenia na innym. Docker spłaszcza te różnice: wszyscy korzystają z tego samego Dockerfile i docker-compose.yml, więc środowisko jest praktycznie identyczne na wszystkich systemach.

W praktyce oznacza to mniej czasu na „polowanie na środowisko”, a więcej na faktyczne debugowanie kodu. Nowa osoba w projekcie nie musi spędzać dnia na konfiguracji – pobiera repozytorium, uruchamia docker compose up i ma gotowe środowisko takie samo jak reszta zespołu.

Jak dobrać właściwy oficjalny obraz PHP z Docker Hub do mojego projektu?

Podstawowy wybór to: wersja PHP, rodzaj serwera i system bazowy. Dla prostych aplikacji wystarczy często php:8.2-apache (PHP z Apache w jednym obrazie). Gdy korzystasz z Nginxa, wybierasz zazwyczaj php:8.2-fpm lub wersję na Alpine, np. php:8.2-fpm-alpine, jeśli zależy ci na lżejszym obrazie.

Jeśli projekt ma określone wymagania (np. działa tylko na PHP 7.4), dobierasz odpowiedni tag, np. php:7.4-fpm. Resztę, czyli doinstalowanie rozszerzeń PHP i bibliotek systemowych, dopisujesz w własnym Dockerfile, dzięki czemu środowisko dokładnie pasuje do potrzeb aplikacji.

Najważniejsze wnioski

  • Docker pozwala traktować środowisko PHP jak lekką, izolowaną aplikację, która startuje w kilka sekund i zużywa znacznie mniej zasobów niż klasyczna maszyna wirtualna.
  • Każdy projekt może mieć własną wersję PHP, własną bazę danych i osobną konfigurację, dzięki czemu znikają konflikty typu „ten WordPress potrzebuje innego MySQL” czy „update PHP zabił legacy serwis”.
  • Powtarzalne środowisko (Dockerfile + docker-compose.yml) uruchamia się identycznie na Windowsie, macOS i Linuksie, więc „u mnie działa” przestaje być wymówką, a błędy łatwiej odtwarzać.
  • Gotowe obrazy i szablony kontenerów skracają czas stawiania nowych projektów – zamiast ręcznie konfigurować Apache/PHP/MySQL, kopiujesz sprawdzoną konfigurację i modyfikujesz tylko szczegóły.
  • Onboarding nowych osób w zespole upraszcza się do jednej komendy: zamiast pół dnia walki z lokalnym serwerem, nowy developer odpala docker compose up i ma to samo środowisko co reszta.
  • Rozróżnienie obrazu (image) i kontenera jest kluczowe: obraz to „instalator” środowiska PHP, a kontener to działająca instancja, którą można swobodnie restartować, usuwać i odtwarzać bez utraty danych trzymanych w wolumenach.
  • Oficjalne obrazy PHP z Docker Hub, połączone z własnym Dockerfile, dają bezpieczną bazę do nauki i produkcji, a jednocześnie pozwalają precyzyjnie dobrać wersję PHP, typ serwera (Apache/FPM) i system bazowy.
Poprzedni artykułNotatki, które chce się czytać: najlepsze metody zapisywania wiedzy
Następny artykułJak wykorzystać sztuczną inteligencję do szybszego zapamiętywania materiału
Halina Olszewski
Halina Olszewski przygotowuje treści dla rodziców, uczniów i nauczycieli, koncentrując się na skutecznych metodach nauki, organizacji pracy oraz rozwoju kompetencji. Szczególną uwagę poświęca temu, jak przekładać wiedzę pedagogiczną i psychologiczną na codzienne, możliwe do wdrożenia działania. Każdy materiał opracowuje w oparciu o sprawdzone publikacje, aktualne zalecenia i praktyczne przykłady, unikając uproszczeń oraz modnych, ale niepotwierdzonych rozwiązań. Jej teksty wyróżniają się spokojnym tonem, rzetelnością i dbałością o to, by czytelnik otrzymał pomocne, bezpieczne wskazówki.