Przejdź do treści

CI/CD dla Next.js z użyciem GitHub Actions

Skonfiguruj automatyczne wdrożenia preview i testy. Zobacz, jak za pomocą GitHub Actions oraz Lighthouse CI zablokować zepsuty i powolny kod przed produkcją.

Maciej Sala

Founder StriveLab

6 min czytaniaOpublikowano 10 stycznia 2026 (Aktualizacja 14 lipca 2026)

Po co CI/CD w projekcie Next.js?

Najmniejsza obietnica brzmi tak: nigdy nie wdrożysz kodu, który nie przechodzi testów albo nie buduje się poprawnie. I to już wystarczy, żeby się opłacało.

Diagram
Struktura pipeline'u: quality jako bramka wejściowa, testy E2E i Lighthouse równolegle, deploy tylko z gałęzi main po przejściu całości.

Podstawowy pipeline CI/CD: lint, test i build

Code
# .github/workflows/ci.yml
name: CI
 
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]
 
# Kolejny push do PR-a anuluje poprzedni, wciąż trwający przebieg
concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true
 
# Token z minimalnymi uprawnieniami i joby, które potrzebują więcej,
# deklarują to u siebie
permissions:
  contents: read
 
jobs:
  quality:
    name: Code Quality
    runs-on: ubuntu-latest
 
    steps:
      - uses: actions/checkout@v4
 
      - uses: pnpm/action-setup@v4
        with:
          version: 9
 
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'pnpm'
 
      - name: Install dependencies
        run: pnpm install --frozen-lockfile
 
      - name: Lint
        run: pnpm lint
 
      - name: Type check
        run: pnpm tsc --noEmit
 
      - name: Unit tests
        run: pnpm test
 
      - name: Build
        run: pnpm build
        env:
          DATABASE_URL: ${{ secrets.DATABASE_URL }}
          NEXT_PUBLIC_APP_URL: https://example.com

Ten pipeline uruchamia się na każdym PR i pushu na main. Jeśli lint, type check, testy lub build failują, to PR jest blokowany.

Dwa bloki na górze pliku wyglądają jak biurokracja, a robią realną robotę. concurrency z cancel-in-progress sprawia, że trzy szybkie poprawki do PR-a nie mielą trzech równoległych pipeline'ów na nieaktualnym kodzie, a na prywatnym repo to wprost oszczędność płatnych minut. permissions: contents: read ogranicza token workflow do odczytu; joby, które potrzebują więcej (jak komentowanie PR-a w sekcji o preview deployments), deklarują to jawnie u siebie. To zasada najmniejszych uprawnień zastosowana do CI, ponieważ gdyby cokolwiek w pipeline zostało skompromitowane, token nie pozwoli na wiele.

Cache zależności i build cache w GitHub Actions

Bez cache każdy przebieg instaluje zależności i buduje od zera, a z cache pipeline skraca się o 50–70%, a przy niezmienionych plikach z minut do krótkiej chwili:

Code
- uses: actions/setup-node@v4
  with:
    node-version: 20
    cache: 'pnpm'
 
- name: Cache Next.js build
  uses: actions/cache@v4
  with:
    path: |
      .next/cache
    key: nextjs-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}-${{ hashFiles('**/*.ts', '**/*.tsx') }}
    restore-keys: |
      nextjs-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}-
      nextjs-${{ runner.os }}-

Testy E2E z Playwright w pipeline CI/CD

Code
e2e:
  name: E2E Tests
  runs-on: ubuntu-latest
  needs: quality # Uruchom po quality check
 
  steps:
    - uses: actions/checkout@v4
 
    - uses: pnpm/action-setup@v4
      with:
        version: 9
 
    - uses: actions/setup-node@v4
      with:
        node-version: 20
        cache: 'pnpm'
 
    - name: Install dependencies
      run: pnpm install --frozen-lockfile
 
    - name: Install Playwright browsers
      run: pnpm exec playwright install --with-deps chromium
 
    - name: Build
      run: pnpm build
      env:
        DATABASE_URL: ${{ secrets.DATABASE_URL }}
 
    - name: Run E2E tests
      run: pnpm exec playwright test
      env:
        BASE_URL: http://localhost:3000
 
    - name: Upload test results
      if: failure()
      uses: actions/upload-artifact@v4
      with:
        name: playwright-report
        path: playwright-report/

Zauważyłeś, że aplikacja buduje się tu drugi raz, a w sekcji Lighthouse, nawet trzeci? To dlatego, że joby na osobnych maszynach nie współdzielą systemu plików, a z cache .next/cache powtórny build niezmienionego kodu trwa ułamek pierwszego. Alternatywa w postaci przesyłania całego folderu .next jako artefaktu bywa wolniejsza niż ponowne budowanie, ponieważ katalog potrafi ważyć setki megabajtów. Zacznij od prostszego rozwiązania z pamięcią podręczną, a przesyłanie artefaktów rozważ dopiero wtedy, gdy budowanie stanie się bardzo ciężkie, a liczba zadań się powiększy.

Code
lighthouse:
  name: Lighthouse CI
  runs-on: ubuntu-latest
  needs: quality
 
  steps:
    - uses: actions/checkout@v4
 
    - uses: pnpm/action-setup@v4
      with:
        version: 9
 
    - uses: actions/setup-node@v4
      with:
        node-version: 20
        cache: 'pnpm'
 
    - name: Install dependencies
      run: pnpm install --frozen-lockfile
 
    - name: Build
      run: pnpm build
 
    - name: Run Lighthouse CI
      uses: treosh/lighthouse-ci-action@v12
      with:
        configPath: './lighthouserc.json'
        uploadArtifacts: true
Code
// lighthouserc.json
{
  "ci": {
    "collect": {
      "startServerCommand": "pnpm start",
      "url": ["http://localhost:3000", "http://localhost:3000/blog"],
      "numberOfRuns": 3
    },
    "assert": {
      "assertions": {
        "categories:performance": ["error", { "minScore": 0.9 }],
        "categories:accessibility": ["error", { "minScore": 0.9 }],
        "categories:best-practices": ["error", { "minScore": 0.9 }],
        "categories:seo": ["error", { "minScore": 0.9 }]
      }
    }
  }
}

Jeśli wynik Performance spadnie poniżej 90 to wtedy pipeline pada, więc wyłapiesz regresje wydajności jeszcze zanim trafią na produkcję. Lighthouse CI sam uruchamia serwer aplikacji dzięki startServerCommand w konfiguracji powyżej, więc nie musisz robić tego osobno.

Jedno zastrzeżenie do progów: współdzielone runnery GitHuba mają zmienną wydajność, więc wynik Performance potrafi się wahać o kilka punktów między przebiegami bez żadnej zmiany w kodzie. Stąd numberOfRuns: 3 (Lighthouse bierze medianę) i stąd rada, by próg ustawić z zapasem względem realnego wyniku — gate na 0.9 przy stronie oscylującej wokół 0.91 będzie failował losowo i nauczy zespół ignorować czerwony pipeline, a to gorsze niż brak gate'a. Dla kategorii, które dopiero doprowadzasz do porządku, użyj poziomu "warn" zamiast "error" — wynik widać w raporcie, ale nie blokuje merge'a.

Przy okazji warto domknąć konfigurację testów E2E z poprzedniej sekcji — żeby Playwright miał uruchomiony serwer, najczyściej zdefiniować webServer w playwright.config.ts:

Code
// playwright.config.ts
import { defineConfig } from '@playwright/test'
 
export default defineConfig({
  use: {
    baseURL: 'http://127.0.0.1:3000',
  },
  webServer: {
    command: 'pnpm start',
    url: 'http://127.0.0.1:3000',
    reuseExistingServer: !process.env.CI,
  },
})

Dzięki temu Playwright sam wystartuje aplikację przed testami w CI, a lokalnie wykorzysta już działający serwer, jeśli go masz.

Deploy Next.js na produkcję

Automatyczny deploy na Vercel

Na Vercel nie potrzebujesz workflow'u deploy, ponieważ Vercel automatycznie deployuje każdy push na main. GitHub Actions używasz tylko do testów.

Samodzielne hostowanie na Coolify lub VPS przez SSH deploy

Code
deploy:
  name: Deploy to Production
  runs-on: ubuntu-latest
  needs: [quality, e2e]
  if: github.ref == 'refs/heads/main' && github.event_name == 'push'
 
  steps:
    - name: Deploy via SSH
      uses: appleboy/ssh-action@v1
      with:
        host: ${{ secrets.SERVER_HOST }}
        username: ${{ secrets.SERVER_USER }}
        key: ${{ secrets.SSH_PRIVATE_KEY }}
        script: |
          cd /opt/apps/nextjs-app
          git pull origin main
          pnpm install --frozen-lockfile
          pnpm build
          pm2 restart nextjs-app
 
    - name: Verify deployment
      run: |
        sleep 10
        curl -f https://example.com || exit 1

Docker deploy aplikacji Next.js

Wdrożenie z użyciem obrazu ma istotną przewagę nad git pull na serwerze: cały proces budowania odbywa się w CI, a na produkcję trafia gotowy, zweryfikowany artefakt. Kluczem do optymalnego obrazu jest ustawienie output: 'standalone' w pliku next.config.ts. Dzięki temu Next.js pakuje w katalogu .next/standalone wyłącznie niezbędne pliki oraz zależności, a rozmiar obrazu spada z ponad gigabajta do około 150 MB:

Code
deploy:
  name: Build & Deploy Docker
  runs-on: ubuntu-latest
  needs: [quality]
  if: github.ref == 'refs/heads/main'
 
  steps:
    - uses: actions/checkout@v4
 
    - name: Login to registry
      uses: docker/login-action@v3
      with:
        registry: ghcr.io
        username: ${{ github.actor }}
        password: ${{ secrets.GITHUB_TOKEN }}
 
    - name: Build and push
      uses: docker/build-push-action@v6
      with:
        context: .
        push: true
        tags: |
          ghcr.io/${{ github.repository }}:latest
          ghcr.io/${{ github.repository }}:${{ github.sha }}
 
    - name: Deploy to server
      uses: appleboy/ssh-action@v1
      with:
        host: ${{ secrets.SERVER_HOST }}
        username: ${{ secrets.SERVER_USER }}
        key: ${{ secrets.SSH_PRIVATE_KEY }}
        script: |
          docker pull ghcr.io/${{ github.repository }}:latest
          docker stop nextjs-app || true
          docker rm nextjs-app || true
          docker run -d --name nextjs-app \
            -p 3000:3000 \
            --env-file /opt/apps/.env \
            --restart unless-stopped \
            ghcr.io/${{ github.repository }}:latest
          docker image prune -f

Trzy istotne zastrzeżenia. Sekwencja stop oraz run oznacza kilka sekund niedostępności przy każdym wdrożeniu, co dla większości stron firmowych jest akceptowalne, jednak przy wymogu braku przerw w działaniu lepiej zastosować narzędzie z wdrożeniem krokowym, takie jak Coolify lub Kamal. Użycie tagu :${{ github.sha }} obok :latest umożliwia natychmiastowy powrót do konkretnej wersji, ponieważ tag :latest oznacza jedynie ostatni zbudowany obraz. Z kolei polecenie docker image prune -f usuwa stare warstwy, zapobiegając sytuacji, w której brak wolnego miejsca na dysku VPS blokuje kolejne wdrożenia.

Preview deployments na Vercel w CI/CD

Każdy PR automatycznie dostaje na Vercel. Aby dodać komentarz z linkiem do PR:

Code
preview-comment:
  name: Preview URL Comment
  runs-on: ubuntu-latest
  if: github.event_name == 'pull_request'
  # Workflow ma globalnie contents: read, więc token job potrzebuje więcej,
  # żeby dodać komentarz
  permissions:
    pull-requests: write
  steps:
    - name: Wait for Vercel
      uses: patrickedqvist/wait-for-vercel-preview@v1.3.2
      id: vercel
      with:
        token: ${{ secrets.GITHUB_TOKEN }}
        max_timeout: 300
 
    - name: Comment PR
      uses: actions/github-script@v7
      with:
        script: |
          const previewUrl = '${{ steps.vercel.outputs.url }}';
 
          await github.rest.issues.createComment({
            issue_number: context.issue.number,
            owner: context.repo.owner,
            repo: context.repo.repo,
            body: `Preview: ${previewUrl}`
          });

Zwróć uwagę na blok permissions w tym jobie, bo workflow globalnie ma token tylko do odczytu, a dodanie komentarza wymaga pull-requests: write. Bez tej deklaracji job skończy się błędem „Resource not accessible by integration", który po prostu oznacza brak uprawnień.

Branch protection: bez tego pipeline to tylko sugestia

Cały pipeline ma sens dopiero po domknięciu ostatniego kroku, o którym artykuły często zapominają: czerwony build sam z siebie niczego nie blokuje. Każdy z uprawnieniami do zapisu może zmergować PR-a z failującymi testami, jeśli repozytorium tego nie zabrania.

W ustawieniach repozytorium (Settings → Branches → Add branch ruleset dla main) włącz dwie rzeczy: Require a pull request before merging, czyli koniec z pushowaniem prosto na main, oraz Require status checks to pass, gdzie z listy wybierasz joby z tego artykułu: Code Quality, E2E Tests, ewentualnie Lighthouse CI. Od tej chwili zielony pipeline przestaje być informacją, a staje się warunkiem merge'a.

Pamiętaj, by wymagać stabilnych jobów. Gdy testy Lighthouse na współdzielonych runnerach bywają niestabilne, ustaw je jako opcjonalne, a jako wymagane sprawdzenia zostaw lint, typy, build i testy. W przeciwnym razie zespół szybko przyzwyczai się do omijania ostrzeżeń, a ochrona kodu stanie się pustym rytuałem.

Testy automatyczne komponentów i E2E w Cypress.
QA & Automation

Często zadawane pytania

Ile trwa typowy pipeline CI dla Next.js?

Z włączonym cache podstawowy zestaw (lint, type check, build, testy jednostkowe) zamyka się zwykle w kilku minutach. Bez cache te same kroki potrafią zająć dwa-trzy razy dłużej, bo za każdym razem instalują zależności i budują od zera. Testy E2E i Lighthouse CI dokładają kolejne kilka minut, bo wymagają uruchomienia aplikacji. Dlatego cache zależności i .next/cache to jedna z pierwszych rzeczy, które warto skonfigurować.

Czy potrzebuję CI/CD dla jednoosobowego projektu?

Tak, choć w okrojonej formie, bo nawet sam lint plus sprawdzenie buildu chroni przed wypchnięciem kodu, który się nie kompiluje albo nie buduje. Takie błędy zdarzają się każdemu, niezależnie od wielkości zespołu. Konfiguracja podstawowego pipeline'u to kwestia kilkunastu minut, a potrafi oszczędzić godziny debugowania na produkcji. Pełny zestaw z E2E i Lighthouse dokładaj wtedy, gdy projekt rośnie i ma realnych użytkowników.

Jak bezpiecznie zarządzać sekretami w GitHub Actions?

Przez wbudowany mechanizm: Settings → Secrets and variables → Actions, gdzie dodajesz wartości takie jak DATABASE_URL, SSH_PRIVATE_KEY czy SERVER_HOST jako zaszyfrowane sekrety. W workflow odwołujesz się do nich przez ${{ secrets.NAZWA }}, a GitHub maskuje je w logach. Uważaj, by nigdy nie dodawać sekretów do commitów, ponieważ raz wypchnięty sekret traktuj jako skompromitowany i zrotuj go, nawet jeśli usuniesz go z historii.

Czy na Vercel potrzebuję osobnego pipeline do deployu?

Nie. Vercel automatycznie wdraża każdy push na main oraz tworzy preview deployment dla każdego pull requesta, więc nie musisz pisać workflow deployującego. GitHub Actions wykorzystujesz wtedy wyłącznie do kontroli jakości (lint, testy, Lighthouse), które mają zablokować merge, jeśli coś jest nie tak. Osobny job deploy piszesz dopiero przy samodzielnym hostowaniu, gdzie to Ty odpowiadasz za dostarczenie kodu na serwer.

Dlaczego job komentujący PR kończy się błędem „Resource not accessible"?

Bo domyślny GITHUB_TOKEN ma coraz częściej uprawnienia tylko do odczytu, a dodanie komentarza wymaga zapisu. Dodaj do joba blok permissions z pull-requests: write (i contents: read). Jawne deklarowanie uprawnień to też dobra praktyka bezpieczeństwa, ponieważ wtedy job dostaje dokładnie to, czego potrzebuje.

Po co concurrency w workflow?

Żeby kolejny push do tego samego PR-a anulował poprzedni, wciąż mielący przebieg. Bez tego po trzech szybkich poprawkach masz trzy równoległe pipeline'y, z których dwa testują nieaktualny kod, a na prywatnych repozytoriach płacisz za każdą zmarnowaną minutę.

Co powinno być wymaganym sprawdzeniem przed mergem?

Minimum to lint, type check i build, ponieważ bez nich nie ma sensu wpuszczać kodu do main. W dojrzalszym projekcie dokładasz testy jednostkowe i E2E, a często także Lighthouse CI jako performance gate. Kluczowe jest oznaczenie tych jobów jako wymaganych sprawdzeń statusu w regułach ochrony gałęzi, bo dopiero wtedy zielony pipeline staje się warunkiem mergowania.

O autorze

Maciej Sala

Maciej Sala — Product Manager i Frontend Developer z bogatym doświadczeniem w marketingu internetowym oraz SEO. Na co dzień pracuje z Reactem, Next.js i TypeScriptem, a ostatnio także z Astro i narzędziami do automatyzacji procesów AI. Sprawnie łączy perspektywę produktową z praktycznym podejściem do kodu. Przez kilka lat był związany z branżą gier wideo jako project manager i game designer. Absolwent historii na Uniwersytecie Jagiellońskim oraz studiów podyplomowych z marketingu internetowego na AGH w Krakowie. Po godzinach trenuje na siłowni, maluje figurki i rozwijam własne projekty.

Pomagam przekładać takie tematy na konkretne wdrożenia w frontendzie, SEO, analityce i procesie produktowym.

Skontaktuj się ze mną

Biblioteka wiedzy na temat Next.js

Czytaj dalej

Zobacz więcej wpisów
Zaawansowane testy E2E z Playwright w Next.js

Playwright idealnie sprawdza się w projektach opartych na Next.js App Router, ponieważ testuje aplikację dokładnie tak, jak widzi ją użytkownik: od uwzględnienia streamingu HTML, przez płynną nawigację, aż po formularze i interakcje. Wbudowane funkcje, takie jak automatyczne oczekiwanie, fixtures, zaawansowane zarządzanie żądaniami oraz testy wizualne, pozwalają kompleksowo zabezpieczyć jakość kodu bez konieczności dokładania kolejnych, zewnętrznych narzędzi.

Maciej Sala

Maciej Sala

Founder StriveLab

Automatyczne testy regresji SEO w GitHub Actions — noindex, link kanoniczny i redirecty pod kontrolą CI/CD

Jedna linijka w niewłaściwym miejscu — , która miała zostać tylko na środowisku testowym — i strona znika z Google na tygodnie. To jeden z najdroższych błędów w SEO technicznym, ponieważ długo pozostaje niewidoczny: build przechodzi, strona działa, użytkownicy niczego nie zauważają, a ruch organiczny po cichu się osuwa. Dobra wiadomość jest taka, że tę klasę błędów da się złapać automatycznie, zanim kod w ogóle trafi na produkcję. W tym artykule pokazuję, jak zbudować testy regresji SEO w Playwright i wpiąć je w GitHub Actions , żeby pull request z zepsutym linkiem kanonicznym czy przypadkowym noindexem po prostu nie przeszedł.

Maciej Sala

Maciej Sala

Founder StriveLab

Vercel vs Coolify vs VPS — gdzie hostować Next.js w 2026?

Next.js można wdrożyć jako statyczny eksport, pojedynczy serwer Node.js, kontener Docker albo aplikację na zarządzanej platformie. Wybór hostingu nie polega więc na pytaniu, czy framework „zadziała”, lecz kto będzie odpowiadał za wdrożenia, cache, skalowanie, bezpieczeństwo, koszty oraz przetwarzanie danych.

Maciej Sala

Maciej Sala

Founder StriveLab