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 UseriBigQuery 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.
- Na hoście uruchomiliśmy
gcloud auth print-access-token, żeby dostać sprawdzony token typu bearer. - W n8n użyliśmy zwykłego noda
HTTP Request. - 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.
-
Test z hosta Uruchomiliśmy
node test_gcloud.jsbezpośrednio na serwerze. Wynik:STATUS_CODE: 200. -
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:
docker compose downw katalogu projektu n8n,docker compose up -d,- 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?
Jak odróżnić błąd IAM, zakresów OAuth i problem sieciowy kontenera?
Kiedy network_mode: host jest potrzebny, a kiedy byłby ryzykownym obejściem?
Jak skonfigurować Traefik po zmianie sposobu podłączenia kontenera do sieci?
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?
docker-compose.yml i plan wycofania zmiany.