Docker to jedno z narzędzi, które w ostatnim czasie najszybciej zyskuje popularność. Dziś jest on podstawą dużej części rozwiązań po stronie programistów, jak i administratorów. W Internecie można znaleźć wiele artykułów oraz tutoriali pokazujących jak korzystać z dockera i jego możliwości, jednak mało mówi się o problemach wynikających z połączenia Docker + Windows.
Jeżeli jesteś jedną z osób które mimo, że podążają krok w krok za najprostszym przykładem nie są w stanie wykonać danego zadania to ten artykuł jest dla ciebie. Docker for Windows boryka się z wieloma bolączkami, których rozwiązanie jest często poza zasięgiem normalnego śmiertelnika. Przykładami mogą być:
-
docker volumes nie działa bez podania żadnego błędu,
-
network i volumes nie działa w docker-compose,
-
mój VPN sprawia, że kontenery przestają działać,
-
docker-compose czasami chce listę jako argument, czasami string,
-
Docker zacina się, kiedy w custom context próbujesz przekopiować za duży folder
Najczęściej można spotkać się ze stwierdzeniem, że zmiana na Hyper-V naprawia większość tych problemów. Jednak z własnego doświadczenia wiem, że wersja ta wprowadza nowe ograniczenia i w większości nic nie zmienia. Mimo że te błędy występują od wielu wersji i są dobrze udokumentowane, to żadne z podanych tam innych rozwiązań (np. downgrade dockera) nie pomoże ze wszystkimi.
Żeby było jasne należy pamiętać o tym, że docker jest przełomowym narzędziem w dzisiejszym rynku IT oraz dalej prężnie rozwijanym projektem open source. Liczę, że kiedyś uda się rozwiązać wszystkie problemy, które występują w wersji na Windowsa, ale trzeba sobie radzić już dziś.
Wcześniej jedynym rozwiązaniem było korzystanie z maszyny wirtualnej z postawionym tam systemem linuxowym. Zdecydowanie utrudniało to jakąkolwiek pracę nie tylko z dockerem, ale z samym programowaniem. Zabierało to również sporą część zasobów komputera, tak potrzebnych w pracy developera. Przy dużych projektach korzysta się z połączenia vagranta i dockera, aby uzyskać spójne środowisko. Natomiast w projektach jednoosobowych i w mniejszych zespołach znacznie łatwiej używać dockera z wsl2.
O samym Windows Subsystem for Linux 2 można przeczytać tutaj. Uogólniając, wsl2 pozwala użytkownikom Windowsa na uruchomienie środowiska Linuxowego bez użycia wirtualnej maszyny. Druga wersja tego narzędzia wprowadziła wiele udogodnień i aktualności w tym pełną integracje z kontenerami dockerowymi. Możliwości jakie daje to narzędzie można już zobaczyć przy najprostszym przykładzie aplikacji napisanej w JavaScript i Node.js
Aby wyzbyć się większości problemów należy otworzyć projekt przy pomocy WSL2. Żeby to zrobić potrzebne będzie:
-
Zainstalowane WSL2 oraz dowolna dystrybucja linuxa (np. Ubuntu), którą można ściągnąć z Microsoft Store.
-
Ściągnąć i zainstalować Dockera z strony projektu. Wraz z najnowszą aktualizacją można to też zrobić na wersji Home systemu Windows. O samej kompatybilność wstecznej można przeczytać tutaj.
-
W Docker Desktop w zakładce resources należy włączyć wsl integration i wybrać zainstalowaną wersje linuxa.
-
Do Visual Studio Code zainstalować dodatek Remote - WSL. Umożliwia on połączenie się z poziomu edytora do subsystemu linuxowego.
Przejdźmy teraz do samego programu. Będzie to prosta aplikacja node.js, która zostanie otwarta przy pomocy dockera i wsl2.
W nowo stworzonym folderze przy pomocy komendy npm init --y
zostaje zainicjonowany projekt i następuje instalacja bibliotek Express.js oraz Nodemon:
npm init -y
npm i express
npm i -g nodemon
Następnie tworze plik app.js. Struktura projektu wygląda następująco:
Jest to prosta aplikacja, która pokazuje wpisany tekst. Celem tutaj nie jest sam program ale pokazanie działania Dockera oraz wsl2. Sam program prezentuje się tak:
const express = require("express");
const app = express();
app.get("/", (req, res) => res.send("Witam ;)"));
app.listen(8080, () => console.log("Server is up!"));
Żeby stworzyć kontener należy utworzyć plik Dockerfile, w którym zawarte są instrukcję, które mają się wykonać w czasie budowania obrazu oraz .dockerignore, gdzie znajdują się ignorowane pliki.
#Dockerfile
FROM node:latest as build-env
ADD . /app
WORKDIR /app
RUN npm install
FROM mhart/alpine-node:latest
COPY --from=build-env /app /app
RUN npm install -g nodemon
WORKDIR /app
EXPOSE 8080
CMD ["nodemon", "app.js"]
#.dockerignore
.git
node_modules
Dockerfile
.dockerignore
W pierwszej linijce określony zostaje obraz, na podstawie którego zostaje zbudowany projekt. Następnie dodawany jest folder /app
, gdzie będzie znajduje się aplikacja. Zaraz potem bieżący katalog ustawiany jest jako katalog roboczy. Komenda RUN npm install
instaluje w kontenerze biblioteki potrzebne do uruchomienia programu.
Wszystkie te kroki zawarte są w części budowy nazwanej build-env. Żeby oszczędzić miejsce na dysku, używa się multi-stage builds, które pozwala na podział fazy budowania na poszczególne etapy.
Druga część Dockerfile opiera się na obrazie apline. Obrazy te są tworzone w taki sposób, żeby miały jak najmniejszy rozmiar. Następnie kopiowana jest część build-env. Znowu ustawiany jest katalog roboczy i określone zostaje, na jakim porcie działa aplikacja (8080).
Aktualna struktura projektu:
Normalnie, wraz z każdą zmianą w kodzie konieczne byłoby przebudowywanie kontenera do nowszej wersji. Staje się to bardzo kłopotliwe przy większych projektach lub kiedy korzysta się z wielu połączonych ze sobą kontenerów poprzez docker-compose, gdzie czas potrzebny na tę operację może sięgać nawet parunastu minut. Aby tego unikać od strony frontendu korzysta się z Nodemona, który automatycznie restartuje serwer z nowymi zmianami. Jednak kiedy wprowadzimy zmiany u siebie na komputerze to zmiany nie zostaną odzwierciedlone w kontenerze.
Aby mieć stuprocentową pewność, że Docker będzie działał poprawnie należy przełączyć się na WSL2. Żeby to zrobić:
- Kliknąć
F1
, wpisać Remote - WSL reopen folder in distro - Otworzyć folder w wybranej wersji linuxa
Okno zostanie przeładowane i projekt zostanie otwarty w wsl2.
Żeby zbudować obraz należy użyć komendy:
docker build -t demo .
W podanym przykładzie demo to tag obrazu, a kropka oznacza bieżący katalog. W terminalu widać poszczególne kroki, które zostały zadeklarowane w pliku dockerfile.
Aby uruchomić aplikacje należy użyć komendy:
docker run -p 8080:8080 -v /mnt/c/Users/folder_na_dysku_c:/app demo
Gdzie:
-
flaga
-p
wskazuje jaki port na komputerze ma odpowiadać portowi z kontenera. -
flaga -v definiuje połączenie pomiędzy folderami na komputerze, oraz folderami z kontenera.
/mnt/c/Users/folder_na_dysku_c
to ścieżka katalogu na moim komputerze natomiast/app
to katalog znajdujący się w kontenerze.
Sama aplikacja prezentuje się następująco:
Teraz każda zmiana wykonana w kodzie spowoduje przeładowanie się projektu i zmiany zostaną od razu odwzorowane na serwerze jak i w kontenerze.
Przedstawione powyżej rozwiązanie pokazuje sposób, w jaki można uniknąć wielu problemów związanych z używaniem Dockera i które osobiście preferuję niezależnie od tego, jaki projekt realizuję. Mam nadzieję, że opisany przykład pomoże, chociaż odrobinę w zrozumieniu działania Dockera + wsl2 i używania ich w sposób bezproblemowy.
Dockerek <3