W tym artykule autor na bazie własnego doświadczenia opisuje jak poprawnie hostować API napisane w NestJS przy użyciu Dockera.
Zagadnienia, jakie będą poruszane to:
- Co to jest NestJS
- Jak przygotować projekt typu API NestJS pod konteneryzację
- Jak przygotować obraz dockerowy dla API NestJS
- Jak uruchomić kontener dockerowy z API NestJS
Co to jest NestJS?
NestJS jest to framework opakowujący technologię NodeJS w architekturę modularną, jaka jest użyta we frameworku frontendowym Angular. NestJS jest polskim projektem napisanym przez Polaka Kamila Myśliwca.
Odwzorowanie Angulara po stronie backendu daje wiele plusów. Po pierwsze sam sposób organizacji dużego projektu zaczerpnięty jest od technologii tak dużej firmy, jaką jest Google, co jest gwarantem jakości rozwiązania. Dodatkowo mocną zaletą jest łatwość nauki frontendowca pracującego w Angularze narzędzia backendowego (i w druga stronę), przekształcając go w Full-Stack Developera.
Różnice pomiędzy dwoma technologiami są głównie na specyficznych dla danego obszaru działania elementach. Po stronie backendu (NestJS) nie zobaczymy komponentów, ponieważ są one związane stricte z widokiem. Po stronie frontendu (Angular) nie zobaczymy kontrolerów, ponieważ one zarządzają żądaniami HTTP.

Mechanizmy modularyzacji oraz wstrzykiwania zależności są w obu technologiach niemal identyczne. Poza tym, jeżeli użyjemy dodatkowych narzędzi do zarządzania rozwiązaniami wieloprojektowymi (np. NRWL Extensions), możemy współdzielić kod pomiędzy dwoma technologiami.
Link do strony projektu: https://nestjs.com/
NestJS i Docker – tworzenie projektu
Przejdziemy teraz do utworzenia API w NestJS i przygotowanie projektu tak, aby był w łatwy sposób używany jako kontener dockerowy. Aby taki kontener był reużywalny przy różnych konfiguracjach, w jakich później będzie pracował, musimy zadbać o kilka spraw. Takimi sprawami są: port, na którym będzie uruchomiona usługa, alias w routingu, czy dowolne parametry potrzebne podczas uruchamiania kontenera z API.
Proces utworzenia bazowego projektu wygląda tak:
- Jeżeli nie mamy, to instalujemy środowisko NodeJS ze strony:
https://nodejs.org/en/download/ - Później instalujemy globalnie dla systemu CLI NestJS poleceniem:
npm
i@nestjs/cli -g
- Tworzymy projekt:
nest new nestjs-docker-example
Podczas tworzenia projektu zostaniemy poproszeni o wybór menadżera pakietów. Wybieramy npm.
Nie będziemy się skupiać nad strukturą projektu, ponieważ nie jest to cel tego artykułu. Skupimy się głównie na sprawach opisanych wyżej tak, aby nasz obraz z API był jak najwygodniejszy do uruchomienia np. przez drugą osobę, która jest DevOpsem i nie chce znać zagadnień deweloperskich przygotowanego obrazu.
NestJS i Docker — Korzystanie ze zmiennych środowiskowych
Tak samo jak w czystym NodeJS – w NestJS możemy dostać się do zmiennych środowiskowych przy pomocy obiektu process.env
. Dzięki temu każda zmienna przekazywana do kontenera podczas uruchomienia może być pobierana z tego obiektu.
Dobrą praktyką jest korzystanie z tych zmiennych w jak najmniejszej ilości plików, bo np. kiedyś wraz z rozwojem technologii może będzie inny, lepszy sposób na dostanie się do zmiennych środowiskowych. Powinno się więc korzystać ze zmiennych:
- w pliku main.ts do skonfigurowania startu API,
- w pliku z głównym modułem app.module.ts do przekazania w jakiś sposób zmiennych w postaci konfiguracji dla innych modułów.
NestJS i Docker – Konfiguracja nasłuchiwania usługi
Podczas uruchamiania API może wyjść potrzeba skonfigurowania nasłuchiwania portu i prefix routingu naszej usługi. Taka potrzeba może wyniknąć ze specyficznej konfiguracji usługi do routingu, która będzie skonfigurowana ponad kontenerem, lub rezerwacji portu dla innego kontenera w tej samej podsieci. Dlatego w pliku main.ts ustawiamy port przekazany ze zmiennej środowiskowej w ten sposób:
await app.listen(process.env.PORT ? process.env.PORT : 3000);
i prefix jeżeli występuje w ten sposób:
if (process.env.PREFIX) app.setGlobalPrefix(process.env.PREFIX);
NestJS i Docker – Przekazywanie konfiguracji przez mechanizm konfiguracji
Do przekazywania wartości zmiennych środowiskowych do innych modułów, kontrolerów i serwisów możemy użyć rozszerzenia do konfiguracji od NestJS. Takie rozszerzenie musimy najpierw zainstalować jako paczkę npm:
npm
i @nestjs/config –save
Później takie rozszerzenie musimy zarejestrować w głównym module app.module.ts:
import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import { AppController } from './app.controller'; import { AppService } from './app.service'; @Module({ imports: [ ConfigModule.forRoot({ isGlobal: true, }), ], controllers: [AppController], providers: [AppService], }) export class AppModule {}
Dodatkowa flaga isGlobal
ustawiona na true
podczas rejestracji pozwala nam na ustawienie tego modułu jako globalny i nie trzeba będzie już go rejestrować w innych modułach.
Teraz już możemy dostać się do zmiennych środowiskowych w innych modułach niż główny, oraz w serwisach i kontrolerach, bez bezpośredniego korzystania z process.env
w tych miejscach. Dla przykładu skorzystamy z tej wartości w wygenerowanym domyślnie serwisie app.service.ts:
import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; @Injectable() export class AppService { constructor(private readonly configService: ConfigService) {} getSiteUrl() { return this.configService.get('SITE_URL'); } }
i pokażemy tą wartość w domyślnym endpoincie w kontrolerze app.controller.ts:
@Controller() export class AppController { constructor(private readonly appService: AppService) {} @Get() getHello(): string { return this.appService.getSiteUrl(); } }
Do testów w głównym katalogu projektu dodamy jeszcze plik .env
, w którym ustawimy zmienną środowiskową:
SITE_URL=http://testowastrona.pl
Teraz możemy już zobaczyć wyniki naszej konfiguracji, uruchamiając API poleceniem:
npm start
i wywołując get request np. w programie Postman: http://localhost:3000/
Zobaczymy wtedy ustawioną zmienną:

Po weryfikacji poprawnie przekazanej zmiennej kończymy proces w konsoli. Po stronie NestJS zostało nam już tylko zbudowanie API. Wykonujemy to poleceniem:
nest build
Bardziej rozbudowane sposoby przekazywania zmiennych przez konfigurację NestJS są opisane na stronie:
https://docs.nestjs.com/techniques/configuration
Przygotowanie obrazu dockerowego dla NestJS
Przygotowanie obrazu rozpoczynamy od utworzenia pliku Dockerfile w głównym katalogu projektu. Do uruchomienia API będziemy potrzebowali środowiska uruchomieniowego NodeJS. Użyjemy do tego bazowego obrazu:
FROM node:16
Ustawiamy tryb produkcyjny środowiska NodeJS ustawiając zmienną:
ENV NODE_ENV=production
Ustawiamy katalog, w którym API będzie pracowało jako katalog dalszych prac w obrazie poleceniem:
WORKDIR /home/node/app
Musimy teraz zainstalować zależności NodeJS w obrazie takie same jak mieliśmy podczas deweloperskiego uruchomienia API. Najpierw kopiujemy pliki package.json i package-lock.json:
COPY ./package* ./
Później już możemy wewnątrz zainstalować zależności:
RUN npm install --prefer-offline --no-audit && \ npm cache clean --force
Patrząc pod kątem bezpieczeństwa zmieniamy jeszcze użytkownika na non-root. W obrazach NodeJS jest przygotowany użytkownik z nazwą node:
USER node
Po ustawieniu użytkownika ostatnim poleceniem przed poleceniem uruchamiającym API jest kopiowanie zbudowanego kodu:
COPY dist .
Kopiowanie zbudowanego kodu jest ostatnie przed uruchomieniem, ponieważ będzie to najczęściej zmieniana warstwa obrazu. Dzięki temu po zmianach w kodzie aplikacji proces budowania obrazu będzie szybszy. Na koniec wprowadzamy już tylko polecenie uruchamiające API w kontenerze:
CMD ["node", "main.js"]
Cały plik Dockerfile wygląda następująco:
FROM node:16 ENV NODE_ENV=production WORKDIR /home/node/app COPY ./package* ./ RUN npm install --prefer-offline --no-audit && \ npm cache clean --force USER node COPY dist . CMD ["node", "main.js"]
Gdy już plik Dockerfile mamy przygotowany, teraz wystarczy tylko zbudować obraz poleceniem:
docker build -t nest-docker .
NestJS i Docker – dobre praktyki przygotowania obrazu
W przedstawionym wyżej przykładzie celowo nie zostało zastosowane podejście Multi Stage Build. Dzięki niemu można uruchomić proces budowania aplikacji (nest build) wewnątrz pliku Dockerfile w pierwszym etapie, a w drugim przygotować obraz do uruchomienia.
Takie podejście jest dobre kiedy nie używamy narzędzia Continuous Integration, a chcemy mieć zawsze takie samo środowisko do budowania. W przypadku kiedy takie narzędzie buduje nam API to ono już zapewnia identyczne środowisko niezależnie od tego kto wyzwolił budowanie kodu.
Budowanie kodu przed zbudowaniem obrazu jest też dobre kiedy budujemy kilka usług korzystających z tych samych bibliotek, np. w ramach takich narzędzi jak Nrwl Extensions. Przyśpiesza to zbudowanie wszystkich obrazów, ponieważ te same biblioteki, które są używane w wielu końcowych usługach, są budowane tylko raz, cachowane i później wykorzystywane w kolejnych usługach.
NestJS i Docker – uruchomienie API
Możemy teraz już uruchomić kontener z API NestJS na podstawie przygotowanego obrazu, przekazując adres do strony, prefix i port poleceniem:
docker run -p 8800:4400 -d --name nest-api --env SITE_URL=
https://zdockera.pl/
--env PORT=4400 --env PREFIX=api2 nest-docker
Wybraliśmy na hoście port 8800, więc zweryfikować działanie API możemy wywołując żądanie typu GET na adresie http://localhost:8800/api2. Jak widać – parametry do API zostały przekazane poprawnie:

Podsumowanie
Do udostępnienia API NestJS możemy użyć Dockera. Sam opis przygotowanego w tym artykule obrazu może być użyty też do przygotowania usług w czystym NodeJS. Ta część przygotowań jest taka sama. API przygotowane w wyżej opisany sposób w obrazie dockerowym może być uruchamiane na Docker Swarm, czy Kubernetes.
Artykuł ten głównie skupia się na uniwersalności uruchomienia oraz poprawnej kolejności warstw obrazu. Ujęte też są główne kwestie bezpieczeństwa obrazów. Taki obraz pomijając oczywiście tematy bezpieczeństwa i inne tematy produkcyjne związane z samym NestJS jest gotowy do uruchomienia w środowisku produkcyjnym.
Adres do Githuba z kodem:
https://github.com/emiljuchnikowski/nestjs-docker-example
Autor

Emil Juchnikowski — pracuje jako Architekt Oprogramowania. Swoją karierę rozpoczął jako .NET Developer ponad 10 lat temu. Teraz już z technologią .NET ma bardzo mało wspólnego. Rozwija się w technologiach JS, w różnych kierunkach: frontend Angular i Ionic, backend NodeJS i NestJS, bazy danych MongoDB. Poza tym zajmuje się też zagadnieniami DevOps, GitOps opartymi o konteneryzację w Kubernetes. Poza pracą lubi biegać na długie dystanse, oraz trenuje boks. Jego motto to: Jedynym ograniczeniem jest nasza wyobraźnia… a niektórzy twierdzą, że jeszcze czas i pieniądze.