Koszmar self-hostingu: błąd 403 w n8n, Google Cloud i Coolify

Self-hosting kusi prostą obietnicą: mniej abonamentów, więcej kontroli. Nie potrzebujesz Heroku, jeśli masz Coolify. Nie musisz płacić za każde wykonanie automatyzacji, jeśli możesz postawić własne n8n. Stack, dane, konfiguracja i koszty są po Twojej stronie.

Brzmi świetnie. Do momentu, w którym przestaje działać.

To historia jednego błędu 403 Forbidden, który na początku wyglądał jak zwykły problem z uprawnieniami. Zadanie było proste: połączyć self-hostowane n8n z Google BigQuery. Finał? Debugowanie sieci Dockera, zachowania proxy i sposobu, w jaki API Google ocenia żądania wychodzące z kontenerów.

Jeśli kiedykolwiek napotkałeś błąd, który wyglądał na niemożliwy do rozwiązania - ten tekst jest dla Ciebie.

Punkt wyjścia: prosty cel, mocny stack

Środowisko wyglądało rozsądnie:

  • serwer w chmurze zarządzany przez Coolify,
  • instancja n8n uruchomiona w kontenerze Docker i opisana w docker-compose.yml,
  • Traefik obsługujący reverse proxy i HTTPS,
  • cel: użyć noda Google BigQuery w n8n i wykonać proste zapytanie SELECT 1.

Przy pierwszym uruchomieniu workflow zatrzymał się na błędzie:

Pojawia się czarny charakter

403. That’s an error.

Your client does not have permission to get URL /bigquery/v2/projects/tantis-XXXXXX/jobs from this server. That’s all we know.

Pierwsza myśl była oczywista: brakuje uprawnień. Jak się okazało, to był fałszywy trop.

Debugowanie krok po kroku

Zamiast zmieniać wszystko naraz, zaczęliśmy sprawdzać kolejne warstwy. Każdy test miał odpowiedzieć na jedno pytanie: czy problem leży w poświadczeniach, konfiguracji n8n, adresie IP, czy w sieci.

Hipoteza 1: błędne poświadczenia albo role IAM

To najbardziej naturalny pierwszy krok. Sprawdziliśmy:

  • typ poświadczeń w n8n - Service Account,
  • poprawność JSON-a konta serwisowego,
  • role IAM w Google Cloud, między innymi BigQuery Job User i BigQuery Data Viewer.

Werdykt: to nie to.

Żeby to potwierdzić, zalogowaliśmy się przez SSH na serwer, aktywowaliśmy dokładnie to samo konto serwisowe przez gcloud CLI i uruchomiliśmy bq query. Zapytanie przeszło bez problemu. Poświadczenia i role były poprawne.

Hipoteza 2: nieprawidłowe zakresy OAuth

Kolejna możliwość: generyczne poświadczenie Google API w n8n nie prosi o właściwy scope. Dodaliśmy ręcznie:

https://www.googleapis.com/auth/bigquery

Werdykt: to nie to.

Błąd 403 nie zmienił się ani trochę.

Hipoteza 3: błąd w obsłudze poświadczeń przez n8n

To był ważny moment. Skoro gcloud potrafił wygenerować działający token, ominęliśmy mechanizm poświadczeń n8n.

  1. Na hoście uruchomiliśmy gcloud auth print-access-token, żeby dostać sprawdzony token typu bearer.
  2. W n8n użyliśmy zwykłego noda HTTP Request.
  3. Wyłączyliśmy w nim autoryzację i ręcznie dodaliśmy nagłówek Authorization: Bearer <token>.

Werdykt: to nie to.

Nawet z poprawnym tokenem żądanie wysłane z n8n nadal kończyło się 403. To zawęziło problem: nie chodziło o konto, role ani token, tylko o samo żądanie wychodzące z kontenera n8n.

Hipoteza 4: blokada IP albo firewall

Komunikat Google sugerował problem “from this server”. Sprawdziliśmy więc, czy kontener wychodzi do internetu z innego publicznego IP niż host.

  • Na hoście: curl ifconfig.me -> 188.245.111.124
  • W kontenerze: docker exec ... node -e "..." -> 188.245.111.124

Werdykt: to nie to.

Publiczny adres IP był identyczny. To wykluczyło prostą blokadę po IP oraz typowe reguły VPC Service Controls oparte na adresie źródłowym.

Test rozstrzygający

Została jedna hipoteza: sama ścieżka sieciowa musi być inna, mimo że z zewnątrz widzimy ten sam adres IP.

Przygotowaliśmy prosty plik test_gcloud.js, który używał natywnego modułu https w Node.js i wykonywał to samo wywołanie API, które próbowało wykonać n8n.

  1. Test z hosta Uruchomiliśmy node test_gcloud.js bezpośrednio na serwerze. Wynik: STATUS_CODE: 200.

  2. Test z kontenera Skopiowaliśmy ten sam plik do działającego kontenera n8n przez docker cp ... i uruchomiliśmy go komendą docker exec ... node /tmp/test_gcloud.js. Wynik: STATUS_CODE: 403.

To był przełom. Ten sam kod, ten sam token i ten sam publiczny adres IP działały z hosta, ale nie działały z kontenera.

Przyczyną okazała się warstwa sieciowa Dockera. Ruch wychodzący z kontenera przechodził inną ścieżką niż ruch z hosta. Najprawdopodobniej był transparentnie obsługiwany przez element warstwy proxy w konfiguracji Coolify/Dockera. API Google widziało to żądanie inaczej niż żądanie wysłane bezpośrednio z hosta - prawdopodobnie przez metadane takie jak X-Forwarded-For - i odrzucało je jako niezaufany relay.

Rozwiązanie: host networking dla n8n

Żeby naprawić problem, ruch wychodzący z kontenera n8n musiał wyglądać dokładnie tak jak ruch z maszyny hostującej. Najprostsza droga: ominąć standardową abstrakcję sieciową Dockera.

Rozwiązaniem było:

network_mode: 'host'

To nie jest jednak zmiana bez konsekwencji. Po przełączeniu n8n na sieć hosta trzeba inaczej ustawić połączenie z bazą danych i ręcznie skonfigurować routing w Traefiku.

1. Zmiana w docker-compose.yml

W usługach n8n i postgresql potrzebne były trzy zmiany.

# docker-compose.yml dla n8n

services:
  n8n:
    image: docker.n8n.io/n8nio/n8n
    network_mode: 'host' # 1. n8n korzysta ze stosu sieciowego hosta
    environment:
      # ... inne zmienne środowiskowe
      # 2. Baza danych jest dostępna przez loopback hosta
      - DB_POSTGRESDB_HOST=127.0.0.1
    volumes:
      - 'n8n-data:/home/node/.n8n'
    # 3. Usuń etykiety Traefika. Nie zadziałają w trybie host.
    #    labels: ...

  postgresql:
    image: 'postgres:16-alpine'
    ports:
      # Port bazy wystawiamy wyłącznie na loopback hosta
      - "127.0.0.1:5432:5432"
    volumes:
      - 'postgresql-data:/var/lib/postgresql/data'
    environment:
      # ... zmienne środowiskowe PostgreSQL

2. Ręczna konfiguracja Traefika

Po usunięciu etykiet Dockera Traefik nie wie już automatycznie, gdzie ma kierować ruch do n8n. Trasę trzeba opisać ręcznie w dynamicznej konfiguracji. W tym przypadku Coolify obserwowało katalog:

/data/coolify/proxy/dynamic/

Utworzyliśmy więc plik:

/data/coolify/proxy/dynamic/n8n.yml

# /data/coolify/proxy/dynamic/n8n.yml

http:
  routers:
    n8n-router:
      rule: "Host(`auto.critical.pl`)"
      service: n8n-service
      entryPoints:
        - "https" # w tym setupie entrypoint nazywa się 'https', nie 'websecure'
      tls:
        certResolver: "letsencrypt"

  services:
    n8n-service:
      loadBalancer:
        servers:
          # specjalna nazwa DNS wskazująca na hosta
          - url: "http://host.docker.internal:5678"

Traefik zarządzany przez Coolify miał już skonfigurowane extra_hosts, więc host.docker.internal poprawnie wskazywał na hosta.

3. Restart usług

Na końcu wystarczyło przeładować usługi:

  1. docker compose down w katalogu projektu n8n,
  2. docker compose up -d,
  3. profilaktycznie: docker restart coolify-proxy.

Po tej zmianie auto.critical.pl wróciło do życia, a node Google BigQuery w n8n zaczął działać poprawnie. Ruch wychodzący z n8n szedł już bezpośrednio przez stos sieciowy hosta, bez problematycznej warstwy pośredniej.

Wniosek: self-hosting daje kontrolę, ale wymaga rozumienia warstw

Ten przypadek dobrze pokazuje cenę self-hostingu. Masz większą kontrolę nad infrastrukturą, kosztami i danymi, ale gdy coś pęka, musisz rozumieć więcej niż samą aplikację.

403 Forbidden nie było tu problemem z uprawnieniami. Było efektem konfliktu między sposobem, w jaki API Google ocenia żądania, a tym, jak kontenerowy stack sieciowy wypuszcza ruch na zewnątrz.

Jeśli więc trafiasz na błąd, który przeczy logice, nie zatrzymuj się na pierwszej hipotezie. Problem nie zawsze leży w aplikacji, tokenie czy konfiguracji usługi. Czasem siedzi w warstwie, której na co dzień nie widać - w sieci, proxy albo sposobie, w jaki kontenery komunikują się ze światem.

FAQ

Dlaczego poświadczenia Google Cloud działają w curl, ale zwracają 403 w n8n?

Najpierw upewnij się, że oba testy używają dokładnie tego samego tokenu, principal, projektu, zasobu i żądania. Kod 403 może oznaczać brak uprawnienia, wyłączone rozliczenia, blokadę albo odpowiedź elementu pośredniego; sam status nie wskazuje przyczyny. Dopiero identyczny test działający na hoście i zawodzący w kontenerze uzasadnia podejrzenie ścieżki sieciowej. Pomaga tabela błędów BigQuery.

Jak odróżnić błąd IAM, zakresów OAuth i problem sieciowy kontenera?

Dla IAM sprawdź w odpowiedzi i Cloud Audit Logs principal, zasób oraz konkretną odmówioną permission. Dla OAuth porównaj tożsamość i zakres tokenu, wykonując ten sam REST request poza n8n. Problem sieciowy rozważ dopiero wtedy, gdy ten sam token, URL i payload dają różny wynik na hoście i w kontenerze; Google opisuje diagnostykę IAM w oficjalnym przewodniku BigQuery.

Kiedy network_mode: host jest potrzebny, a kiedy byłby ryzykownym obejściem?

Użyj go dopiero jako kontrolowanej odpowiedzi na powtarzalny test A/B, nie jako pierwszy sposób naprawy każdego 403. Tryb host współdzieli stos sieciowy hosta, nie nadaje kontenerowi własnego IP i ignoruje mapowanie portów; zmniejsza izolację oraz zwiększa ryzyko kolizji portów. Ograniczenia opisuje Docker Host network driver.

Jak skonfigurować Traefik po zmianie sposobu podłączenia kontenera do sieci?

Traefik musi dostać jawny router i service kierujący do portu n8n osiągalnego na hoście, na przykład przez host.docker.internal:5678; sprawdź też nazwę entrypointu i resolver certyfikatu w swoim proxy. W Coolify dynamiczną konfigurację można dodać z ustawień proxy, a katalog /data/coolify/proxy/dynamic jest używany przez konfiguracje plikowe. Aktualny wzorzec pokazuje dokumentacja load balancingu Coolify.

Jakie logi i testy zebrać przed zmianą konfiguracji produkcyjnej?

Zapisz pełne body błędu bez sekretów, reason lub request ID, czas, principal, projekt, dataset i wersję n8n. Uruchom identyczny curl lub skrypt Node na hoście i w kontenerze, porównaj DNS, TLS, proxy i publiczny egress, a potem zbierz log wykonania n8n, kontenera, Traefika oraz odpowiedni wpis Cloud Audit Logs. Gdy logi są zbyt skąpe, sprawdź ustawienia logowania n8n; zachowaj też diff docker-compose.yml i plan wycofania zmiany.

Gotowy na strategiczne partnerstwo w marketingu cyfrowym?

Porozmawiajmy o Twoich wyzwaniach i celach. Opracujemy dopasowaną strategię i plan działania, które przyniosą mierzalne rezultaty dla Twojego e-commerce lub instytucji.