Podczas mojego wdrożenia jeden z programistów powiedział mi półżartem, że odkąd dołączył do projektu, przestał wyłączać komputer. Po prostu go usypiał, bo nie uśmiechała mu się perspektywa otwierania 15 terminali następnego ranka, żeby uruchomić aplikację.
Produkt składał się z frontendu w Next.js, backendu w NestJS, Keycloaka do uwierzytelniania, 11 workerów w Node.js, PostgreSQL i Redisa. Część działała w Dockerze, część bezpośrednio na komputerze. Różne usługi używały różnych wersji Yarna.
I, jak to często bywa z projektami, które zaczynały jako MVP, dokumentacji było niewiele, wiedza siedziała w głowach programistów, a pytanie o proces wdrażania zwykle wywoływało nerwowy uśmiech.
Podczas onboardingu zapytałem kolegę, który mnie wdrażał, dlaczego aplikacja sypie błędami przy starcie.
Kolega: "A tak... Zapomniałem o tym. Musisz zmienić flagę w pliku .env. Wiemy o tym, to dług techniczny."
Nico: "Okej, mamy te rzeczy gdzieś opisane? Chociaż w zgłoszeniach na GitHubie?"
Kolega: "Nie bardzo... ale powinniśmy zacząć."
To dość dobrze podsumowywało sytuację.
Żeby uruchomić całość, trzeba było przejść 17 kroków onboardingu, otworzyć około 15 terminali i pamiętać o zestawie wyjątków, które najwyraźniej każdy miał po prostu znać.
Szczerze? Jedno z najgorszych środowisk deweloperskich, z jakimi się spotkałem.
Produkt urósł. Proces musiał za nim nadążyć.
Dołączyłem w sierpniu 2026 roku. Produkt już obsługiwał dane finansowe prawdziwych firm. Raczej nie ten rodzaj oprogramowania, w którym chcesz odkrywać problemy z wdrożeniem dopiero na produkcji.
Wadliwe wydanie mogło uderzyć w klientów i reputację właściciela. A przy tylu ręcznych krokach każde wdrożenie zależało od tego, czy ktoś zapamiętał właściwą procedurę.
Nic dziwnego, że za każdym razem wszyscy obchodzili się z tym jak z jajkiem.
Dotychczasowa konfiguracja pochodziła z okresu, kiedy priorytetem było jak najszybsze zbudowanie MVP. Spełniła swoje zadanie. Teraz firma potrzebowała powtarzalnych wdrożeń i systemu, który docelowo można byłoby uruchamiać także na serwerach klientów.
Zatrudniono mnie do trzech rzeczy:
- Zamienić wdrażanie w udokumentowaną, powtarzalną procedurę.
- Skrócić czas budowania aplikacji.
- Uprościć uruchamianie całego systemu.
Po wstępnym rozpoznaniu i onboardingu zaproponowałem podejście oraz oczekiwany rezultat. Kierownik projektu po stronie klienta zaakceptował plan, a ja odpowiadałem za całą realizację aż do oddania prac w połowie września.
W tym czasie pozostałych czterech programistów dalej rozwijało funkcje i naprawiało błędy. Musieliśmy to zrobić bez zatrzymywania rozwoju produktu.
Najpierw sprawmy, żeby samo rozpoczęcie pracy nie było projektem
Podczas rozpoznania jedna rzecz stała się jasna: nie każdy potrzebował przez cały czas uruchomionego całego systemu.
Osoba pracująca nad frontendem nie potrzebowała 11 workerów zużywających zasoby. Ktoś zmieniający logikę workerów już tak. A od czasu do czasu potrzebowaliśmy odtworzyć zachowanie aplikacji na produkcji.
Wprowadziłem więc cztery tryby uruchamiania:
| Tryb | Co uruchamia |
|---|---|
hybrid-app |
Główne aplikacje z hot reloadem. PostgreSQL, Redis i Keycloak w Dockerze. Bez workerów. |
hybrid-all |
Ten sam zestaw, ale ze wszystkimi workerami uruchomionymi lokalnie. |
prod-app |
Główne usługi jako produkcyjne obrazy Dockera. Bez hot reloadu i workerów. |
prod-all |
Cały system jako produkcyjne obrazy Dockera, ze wszystkimi workerami. |
Instrukcja uruchomienia sprowadzała się do:
- Sklonuj repozytorium.
- Uruchom jedną komendę, która utworzy lokalne pliki środowiskowe na podstawie dołączonych przykładów.
- Wybierz tryb i uruchom aplikację.
Był też drugi powód, żeby umieścić cały system w kontenerach.
Klient planował umożliwić uruchamianie aplikacji na własnych serwerach. Nie był to pilny priorytet, ale chciał mieć taką możliwość w przyszłości.
Skoro już przebudowywałem sposób uruchamiania i działania aplikacji, warto było uwzględnić ten wymóg od razu. Te same zmiany, które ułatwiły pracę lokalną, przygotowały też aplikację do działania poza dotychczasową chmurą.
Początkowo część programistów podchodziła do zmian niechętnie. Znali stary proces i nie widzieli szczególnej potrzeby, żeby go ruszać. Zakładałem, że chodzi o brak czasu na wdrożenie, a nie o to, że sama propozycja im nie odpowiada.
Okazało się, że się myliłem.
Bali się zamienić uciążliwy, ale przewidywalny setup na coś, co rozwali im lokalne środowisko po zmergowaniu któregokolwiek z moich PR-ów. Nikt nie chce spędzać pierwszej godziny dnia na debugowaniu Dockera zamiast pracy nad produktem.
Szczerze, rozsądna obawa. Trudno mieć im to za złe.
Dlatego najpierw oddałem mały pilotaż. Sama główna aplikacja w trybie hybrydowym, bez workerów. Coś, czego mogli używać na co dzień, gdy ja budowałem resztę.
Pytałem o feedback na Slacku i wracałem do tematu na codziennych spotkaniach. Pewnie częściej, niż by sobie życzyli.
Ale zaczęli tego używać. I ku mojemu zaskoczeniu, a pewnie też ich, nowy setup spodobał im się jeszcze bardziej, niż zakładałem.
To była cenna lekcja, do której często wracam.
Wydanie wersji nie powinno zależeć od tego, kto pamięta wszystkie kroki
Wcześniej programiści budowali obrazy aplikacji lokalnie, ręcznie wysyłali je do Google Cloud i ręcznie wykonywali tam migracje bazy danych. Brzmi strasznie? Było strasznie.
W trakcie prac odkryliśmy też, że schematy baz testowej i produkcyjnej zaczęły się różnić. Testowanie na innym schemacie utrudnia ocenę, co właściwie potwierdza test, który przeszedł.
Przeniosłem zmiany bazy do wersjonowanych plików migracji PostgreSQL w repozytorium. Migracje działały wyłącznie do przodu. Środowiska mogły korzystać z tej samej historii zmian, a schemat dało się odtworzyć od zera. Dla baz z istniejącymi danymi musiałem przygotować dodatkowe migracje i ręcznie wyrównać różnice między bazami.
Następnie zbudowałem proces wydawania wersji oparty na GitHub Actions i Google Cloud.
Wdrożenie zaczynało się od wybrania commita z main. Google Cloud budował obrazy, ponieważ nasze pomiary pokazały, że w naszym przypadku wychodziło to szybciej i taniej.
Dalej proces:
- Wykonywał oczekujące migracje bazy danych.
- Tworzył nową instancję aplikacji, do której nie trafiał ruch klientów.
- Uruchamiał testy end-to-end pod adresem tej instancji.
- Zostawiał przetestowaną wersję gotową do osobnego kroku przekierowania ruchu.
Jeżeli którykolwiek krok się nie udał, proces zatrzymywał się i zachowywał logi oraz artefakty potrzebne do diagnozy. Nowa wersja nie dostawała automatycznie ruchu klientów.
Decyzją klienta przekierowanie ruchu pozostało ręczne. Automatyzacja miała usuwać powtarzalną pracę i ograniczać ludzkie błędy, ale decyzja o udostępnieniu wersji klientom nadal należała do zespołu. Zespół mógł określić, jaki procent ruchu na nią skierować, zacząć od małej części użytkowników albo sam wejść na nową wersję i jeszcze coś sprawdzić.
Na środowisko testowe wdrażaliśmy mniej więcej 2–3 razy w tygodniu, na produkcję 1–2. Nie potrzebowaliśmy budować i wdrażać każdej zmiany, która trafiała na main.
Powrót do poprzedniej wersji też potrzebował jasnej procedury
Poprzednie wersje aplikacji pozostawały dostępne, więc osobna akcja w GitHubie mogła skierować ruch z powrotem na wcześniejszą wersję. Znaleźliśmy błąd? Uruchamiamy rollback ręcznie. Wszyscy zadowoleni. Proste.
Trudniejsza była baza danych. Nasze migracje plikowe działały tylko do przodu i z założenia nie chcieliśmy ich odwracać. Uzgodniliśmy, że zmiany mają zachowywać zgodność z poprzednią wersją aplikacji, i dodaliśmy kontrole kompatybilności w CI, żeby wspierały ten proces wydawania.
Dodałem też testy obciążeniowe przez GitHub Actions, z możliwością ręcznego uruchomienia ich na wdrożonym środowisku. Były szczególnie przydatne przy zmianach logiki workerów lub zapytań do bazy. Mogliśmy sprawdzić, czy zmiana wytrzyma oczekiwane obciążenie, zanim uznamy to za pewnik.
Udokumentowałem procedury migracji, przekierowania ruchu, rollbacku i diagnozowania nieudanych kroków. Dokumentacja przyjazna programistom i gotowa do pracy z agentami AI.
Siedemnaście minut to sporo czekania
Pełny lokalny build trwał początkowo około 17 minut na MacBooku Air M1 z 16 GB RAM-u. Nie był to najnowszy sprzęt, ale dobry punkt odniesienia do tego, na czym programista rzeczywiście może pracować.
Wprowadziłem cache buildów Dockera, zmieniłem konfigurację budowania i uruchamiałem niezależne kroki równolegle tam, gdzie było to możliwe. Dodałem też Turborepo i Turbopack, żeby ograniczyć zbędną pracę podczas buildów.
Pełny build skrócił się z 17 minut do 4. 4,25 raza szybciej. Czas budowania krótszy o 76,5%.
Ta liczba dotyczy samego budowania, a nie całego wdrożenia. Testy, migracje i decyzja o przekierowaniu ruchu były osobnymi krokami.
Cztery osoby cały czas zmieniały kod, nad którym pracowałem
Poszczególne zmiany techniczne nie były fizyką kwantową. Wyzwaniem było sprawić, żeby wszystko działało razem, gdy 4 programistów nadal dostarczało nowe funkcje. Najwięcej uwagi wymagały koordynacja i komunikacja.
Pracowaliśmy w trunk-based development, często mergując zmiany do main. Moje zmiany platformowe musiały współgrać z pracą zespołu nad produktem. Zmiana zależności mogła wymagać ponownego buildu. Inna zmiana mogła wymusić reset lokalnej bazy. Do tego dochodziły terminy Product Ownera, który chciał widzieć nowe funkcje i postępy, oraz ustalone czasy reakcji na pojawiające się zgłoszenia.
Bez jasnych instrukcji ktoś mógł pobrać działający kod i spędzić poranek na zastanawianiu się, dlaczego aplikacja nie chce mu się uruchomić.
Pracowaliśmy asynchronicznie, więc instrukcje umieszczałem w PR-ach: co się zmieniło, co uruchomić po mergu i czego się spodziewać. Do tego wiadomość na naszym kanale aktualizacji na Slacku z linkiem do PR-a.
Delikatna operacja, delikatnie mówiąc. Skłamałbym, mówiąc, że nie było problemów. Jasne, że były. Przy tak szybkim rozwoju to normalne. Ale mogę z dumą powiedzieć: zmieściliśmy się w 6 tygodniach i spełniliśmy wszystkie wymagania biznesowe.
Wtedy dołączył ktoś nowy
Po wdrożeniu zmian potrzebowaliśmy potwierdzenia, że zrozumie je ktoś nowy. Cała dokumentacja byłaby bez sensu, gdyby nie dało się z niej skorzystać. Oznaczałoby to, że proces się zmienił, ale szczegóły nadal siedzą w głowach programistów. Niedobrze.
I trafiła się idealna okazja: dołączył nowy programista.
Wskazałem mu dokumentację i poprosiłem, żeby spróbował skonfigurować aplikację bez mojej pomocy. Jeśli coś było niejasne, chciałem o tym wiedzieć, póki mogłem to poprawić. Nie zostawiłem go samego z problemami. Poprosiłem, żeby od razu odezwał się do mnie, gdyby na coś trafił.
Powiedział mi, że od sklonowania repozytorium do zobaczenia działającej aplikacji na swoim komputerze minęło około 15 minut.
Na przeczytanie dokumentacji i zrozumienie lokalnego procesu pracy poświęcił około półtorej godziny. Nie musiał zadać mi ani jednego pytania o konfigurację.
Pełny, bezdyskusyjny sukces. Z tej części cieszę się najbardziej.
Ktoś, kto nigdy wcześniej nie dotykał projektu, mógł dołączyć, wykonać instrukcje i zacząć pracę. Dotychczasowy zespół już korzystał z nowego rozwiązania. Wydawanie wersji miało procedurę, którą można było przeczytać i powtórzyć.
A uruchomienie aplikacji rano w końcu przestało być problemem.