Ręczne przeciąganie motywów przez FTP w 2026 roku kończy się zwykle tak samo: brakuje jednego pliku z inc/, composer.lock jest z innego laptopa, a produkcja leży, bo ktoś nadpisał wp-config.php. Continuous Integration i Continuous Deployment nie są fanaberią enterprise - to sposób, by każda zmiana w Gicie przeszła ten sam build, te same testy i ten sam sposób wgrania na serwer.
Ten przewodnik dotyczy WordPressa z motywem lub wtyczką w repozytorium (Composer, npm, ewent. Docker), nie „strony klikanej wyłącznie w panelu”. Jeśli cały kod żyje tylko w bazie i w mediach, najpierw przenieś motywy i custom code do Gita - bez tego CI/CD nie ma czego budować.
Oficjalna dokumentacja runnerów: GitHub Actions. Twierdzenie WordPressa o hardeningu serwera warto czytać równolegle: Hardening WordPress.
Dlaczego FTP i „wrzuć na FTP” odpadają
Trzy problemy wracają w audytach polskich sklepów WooCommerce i stron agencji:
- Niespójność środowiska - lokalnie PHP 8.2 i Composer 2.7, na VPS PHP 8.1 bez rozszerzenia
intl. Lokalnycomposer install„działa”, produkcja pada na class not found. - Brak audytu - po incydencie nie da się powiedzieć, który commit był na żywo. Panel hostingu pokazuje datę pliku, nie SHA z Gita.
- Sekrety na dysku - zapisane hasła SFTP w FileZilli albo w
.envcommitowanym „na chwilę” to klasyczna droga do wycieku.
CI/CD rozwiązuje to mechanicznie: build na znanym obrazie (np. shivammathur/setup-php albo własny Docker), artefakt wersjonowany SHA, deploy po SSH kluczem trzymanym w Secrets.
Minimalny potok: push, build, test, deploy
Typowy workflow dla motywu custom + wtyczek z Composer:
- Trigger - push do
mainalbo merge Pull Requestu po review. Gałąźdevelopczęsto idzie tylko na staging. - Build -
composer install --no-dev --optimize-autoloader,npm ci,npm run build. Wersje PHP i Node pinujesz w workflow (php-version: '8.2',node-version: '20'). - Test - PHPUnit dla wtyczek, ewent. Playwright na krytyczny flow koszyka. Fail = stop, zero deployu.
- Artefakt - pakujesz katalog release bez
.git, beznode_modules, zvendor/i zbudowanymassets/dist/. - Deploy - rsync/SCP do
releases/<run_id>/, potem atomowe przełączenie symlinku.
Fragment idei (skrócony) w GitHub Actions:
name: deploy-production
on:
push:
branches: [main]
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2
with:
php-version: '8.2'
- run: composer install --no-dev --optimize-autoloader
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: npm
- run: npm ci && npm run build
- name: rsync release
env:
SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
run: |
# zapisz klucz, rsync do releases/$GITHUB_SHA, ln -sfnPełny skrypt symlinku trzymasz w repo jako bin/release.sh - w workflow tylko go wywołujesz. Dzięki temu ten sam skrypt działa z laptopa w awarii, gdy Actions leży.
Wdrożenia atomowe zamiast nadpisywania plików
Nadpisywanie plików „w miejscu” na public_html oznacza, że przez kilka sekund połowa requestów widzi stary functions.php i nowy template. Przy WooCommerce to potrafi zepsuć sesję koszyka.
Model atomowy:
- katalog bazowy:
/var/www/site/ - releases:
/var/www/site/releases/20260920-142211/ - shared: uploads,
wp-config.php, cache object - poza release current→ symlink do aktywnego release
Po udanym rsync:
ln -sfn /var/www/site/releases/20260920-142211 /var/www/site/currentNginx root wskazuje na .../current/public (albo odpowiednik). Przełączenie jest natychmiastowe. Stare release kasujesz po N dniach albo zostawiasz ostatnie trzy na rollback.
WordPress uploads muszą być poza release - inaczej każdy deploy kasuje media albo wymaga kopiowania gigabajtów. Klasyczny układ: shared/uploads linkowany do wp-content/uploads w każdym release.
Staging, preview i macierz strategii
W 2026 roku ścieżka feature branch → PR → staging → main/production nie jest opcjonalna dla sklepów i stron leadowych. Staging powinien mieć:
- tę samą wersję PHP i rozszerzeń co produkcja
- kopię bazy (anonimizowaną, jeśli są dane osobowe RODO)
- te same sekrety płatności w trybie sandbox
Preview per PR (tymczasowy URL) ma sens przy redesignie i przy zmianach CSS, mniej przy migracjach schematu bazy - te i tak testujesz na stagingu z prawdziwą kopią.
| Strategia | Ryzyko | Koszt utrzymania | Kiedy ma sens |
|---|---|---|---|
| Ręczne FTP | Bardzo wysokie | Niski start, wysoki koszt awarii | Tylko sandbox / nauka |
| Git hook na serwerze | Średnie | Niski | Małe portfolio, jeden deweloper |
| CI + rsync atomowy | Niskie | Średni | Biznes, WooCommerce, agencje |
| Blue/green lub dwa VPS | Minimalne | Wysoki | Duży ruch, SLA, płatności |
Blue/green (dwa pełne środowiska, przełączenie load balancera) rzadko opłaca się poniżej kilkuset równoległych sesji. Dla większości polskich sklepów wystarcza atomowy symlink + rollback katalogu.
Sekrety, WP-CLI i baza danych
Nigdy nie commituj:
- kluczy SSH
- haseł bazy
AUTH_KEY/SECURE_AUTH_KEYzwp-config.php- tokenów API bramek płatności
W GitHubie: Settings → Secrets and variables → Actions. Wstrzykujesz je jako env w kroku deploy. Na serwerze wp-config.php leży w shared/ i nie jest częścią artefaktu z CI.
WP-CLI w potoku po przełączeniu symlinku:
wp cache flush(jeśli object cache)wp rewrite flushpo zmianie CPTwp plugin listjako smoke w logu
Migracje bazy (custom tabele, ACF JSON już w repo) planuj osobno od deployu plików. Rollback symlinku nie cofa ALTER TABLE. Jeśli migracja jest destrukcyjna, najpierw backup (wp db export na stagingu i produkcji), potem migracja, potem dopiero przełączenie ruchu na kod, który jej wymaga - albo feature flag w PHP.
Dokumentacja instalacji zależności PHP: Composer install - w CI zawsze --no-dev na produkcji i zablokowany composer.lock.
Co najczęściej psuje pierwszy potok WordPress
Z praktyki wdrożeń na VPS (Hetzner, DigitalOcean, własne OVH) i na managed z SSH:
.gitignorezjadavendor/- produkcja dostaje motyw bez autoloadera. Albo budujesz vendor na runnerze i wrzucasz do artefaktu, albo uruchamiasz Composer na serwerze w kroku po rsync (wtedy serwer musi mieć Composer i sieć do Packagist).- różne
ABSPATH/ ścieżki - skrypt zakłada/var/www/html, a hosting ma/home/user/domains/.... Pinuj ścieżki w Secrets per środowisko. - opcache trzyma stary kod - po
ln -sfnwywołaj reload PHP-FPM alboopcache_resetprzez WP-CLI/mu-plugin tylko dla deploy usera. - cron i kolejki - Action Scheduler WooCommerce może odpalać joby na starym kodzie przez kilka minut; po dużym release warto chwilę obserwować failed jobs.
Smoke test po deployu (curl z runnera albo osobny job):
- strona główna HTTP 200
/wp-login.php200- jeden krytyczny URL produktu lub landing z formularzem
- opcjonalnie HEAD do CDN, jeśli assety mają hash w nazwie
Jeśli smoke padnie, automatyczny rollback symlinku jest tańszy niż telefon od klienta o 23:00.
Motywy blokowe, mu-pluginy i wyjątki od „pełnego Gita”
Nie cały WordPress nadaje się do tego samego potoku. Treści w bazie (wpisy, ACF field groups jeśli nie eksportujesz JSON, Elementor CSS) i tak żyją poza release. CI/CD chroni kod, nie zastępuje backupu bazy.
Praktyczne podziały:
- Motyw klasyczny lub hybrydowy z Vite - pełny build w CI, artefakt z
style.css+dist/. - Block theme (FSE) - trzymasz
theme.jsoni szablony HTML w Gicie; eksport zmian z Site Editora wraca do repo przez PR, inaczej produkcja i Git rozjeżdżają się po pierwszym kliknięciu „Zapisz”. - Must-use pluginy - wgrywane razem z release; nie aktualizujesz ich z panelu. To dobre miejsce na healthcheck deployu i wyłączenie file editora.
- Wtyczki z wordpress.org - albo pinujesz wersje w Composer (
wpackagist), albo aktualizujesz je osobnym jobem z review. Mieszanie „panel kliknął Update” z „CI nadpisał katalog” kończy się losowym stanemplugins/.
Na stronach agencji w PL często widzimy hybrydę: custom motyw + WooCommerce z wpackagist + dwie wtyczki premium wgrane jako zip do private/ i instalowane Composerem z path repository. Potok musi znać wszystkie trzy źródła, inaczej staging i produkcja różnią się jednym ZIP-em sprzed pół roku.
Checklist przed pierwszym deployem z Actions
Zanim podłączysz main do produkcji, przejdź tę listę raz - oszczędza weekend:
- Repozytorium prywatne albo bez sekretów w historii (
git log -pnie pokazuje starych.env). - Branch protection: wymagany PR, wymagany zielony workflow
test. - Osobne Secrets:
DEPLOY_HOST_STAGING,DEPLOY_HOST_PROD, osobne klucze SSH. - Staging zsynchronizowany z produkcją co do wersji PHP (nie „u nas działa 8.3, u klienta 8.1”).
- Backup bazy przed pierwszym atomowym przełączeniem - nawet jeśli rollback kodu jest gotowy.
- Monitoring uptime (UptimeRobot, Better Stack, cokolwiek) z alertem SMS/Slack na URL produkcyjny.
- Runbook na jednej stronie: jak ręcznie
ln -sfnna poprzedni release, kto ma dostęp do serwera.
Dopiero potem włączasz auto-deploy z main. Wcześniej wystarczy workflow „build + test” na PR i ręczny workflow_dispatch na staging.
Podsumowanie
CI/CD dla WordPressa to nie „magiczny YAML”, tylko powtarzalny kontrakt: ten sam PHP, ten sam lockfile, ten sam artefakt, przełączenie bez okna z połową plików. Zaczynasz od Gita i stagingu, dokładasz Actions z buildem Composer/npm, kończysz atomowym current i smoke testem. FTP zostaje narzędziem awaryjnym, nie procesem.
Jeśli budujesz taki potok pod sklep lub stronę leadową i potrzebujesz kogoś, kto utrzyma go razem z kodem motywu, napisz przez formularz kontaktowy - opisujemy zakres wdrożenia i utrzymania CI/CD przy realnym stacku WordPress.







