W dzisiejszych czasach Docker jest podstawowym narzędziem warsztatu programisty. Dzięki niemu łatwiej jest nam tworzyć aplikacje rozproszone niezbędne np. przy wykorzystaniu architektury mikroserwisowej. Łatwiej jest nam również weryfikować poprawność działania tworzonego oprogramowania bez konieczności posiadania zewnętrznych serwerów. Wszystko możemy sprawdzić na lokalnej maszynie. Z tego powodu chciałbym przedstawić w tym artykule jak możemy wykorzystać Dockera do uruchamiania aplikacji Spring Boot’owej. Będziemy oczywiście do tego potrzebowali zainstalowanego Dockera oraz projekt wykorzystujący Spring Boot’a.

Klasyczne podejście - packaging WAR

Na początku musimy stworzyć szkielet aplikacji przy pomocy strony String Initializr. Wybierzmy zależność do ‘Spring Web’ oraz dajmy sposób pakowania na WAR.

Tworzenie aplikacji Spring Boot w opaciu o WAR

Gdy już rozpakujemy projekt i zaciągniemy go do wybranego IDE musimy utworzyć jakiś endpoint, aby zweryfikować czy nasza aplikacja działa poprawnie. Zróbmy to poprzez utworzenie kontrolera, który będzie zwracał napisany przez nas literał.

1
2
3
4
5
6
7
8
9
10
11
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
class WarController {

  @GetMapping("/welcome")
  String welcome() {
    return "Welcome from WAR Java app";
  }
}

Dodajmy jeszcze linijkę w pom.xml, żeby uzyskać przyjemniejszą nazwę wygenerowanej paczki aplikacyjnej.

1
2
3
4
<build>
  <finalName>${artifactId}</finalName>
  ...
</build>

Wybraliśmy na początku, że ma być to projekt Maven, dlatego użyjmy polecenia ./mvnw clean package, aby utworzyć paczkę WAR.

Wygenerowany plik WAR

W celu uruchomienia aplikacji spakowanej do WAR będziemy potrzebowali kontenera aplikacji sieciowych jakim jest np. Tomcat. Tu właśnie z pomocą przychodzi nam Docker. Dzięki niemu nie musimy się martwić żadną konfiguracją Tomcat’a. Po prostu zaciągniemy gotowy obraz i tyle! Zobaczymy jak to wygląda zaczynając od upewnienia się, że Docker działa prawidłowo. Wpiszmy do terminala zapytanie o wersję Dockera.

1
2
$ docker --version
Docker version 20.10.7, build f0df350

W poszukiwaniu odpowiedniego obrazu Docker’owego

Teraz musimy wejść na stronę hub.docker.com i sprawdzić czy istnieje tam obraz Tomcat’a.

Poszukiwanie obrazu Tomcata

Jak widać jest on dostępny. Od razu dostajemy podpowiedź, żeby użyć komendy docker pull tomcat. W ten sposób pobierzemy obraz na naszą lokalną maszynę. My jednak użyjemy innej komendy, która pobierze dla nas obraz i od razu uruchomi kontener. Domyślnie pobierze nam się widoczna wersja oznaczona tagiem latest.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$ docker run -d -p 8080:8080 tomcat
Unable to find image 'tomcat:latest' locally
latest: Pulling from library/tomcat
0bc3020d05f1: Pull complete
a110e5871660: Pull complete
83d3c0fa203a: Pull complete
a8fd09c11b02: Pull complete
96ebf1506065: Pull complete
b8bf70f9cc4d: Pull complete
57a728160d21: Pull complete
ae090d34063a: Pull complete
a0f1070cb1d8: Pull complete
d5e3624ecc80: Pull complete
Digest: sha256:87f1ba31ed70027e46c926a5b25710f99256cdaa254e51359e5ab84792e8b28a
Status: Downloaded newer image for tomcat:latest
179947e64f490db4f585114d3d1dd0383a947f5b75e380dd2e860a8dc21776c5

$ docker container ls
CONTAINER ID   IMAGE     COMMAND             CREATED          STATUS          PORTS                                       NAMES
179947e64f49   tomcat    "catalina.sh run"   38 seconds ago   Up 26 seconds   0.0.0.0:8080->8080/tcp, :::8080->8080/tcp   crazy_montalcini

Wykorzystując komendę docker run -d -p 8080:8080 tomcat pobraliśmy obraz Tomcat’a, uruchomiliśmy kontener mapujący port 8080 lokalnej maszyny z portem 8080 kontenera, który przy pomocy flagi -d został uruchomiony w tle. Dostaliśmy komunikat, że obraz tomcat:latest nie istnieje u nas lokalnie, więc Docker musiał go pobrać. Na koniec używając komendy docker container ls dowiadujemy się, że nasz kontener działa prawidłowo.

Wrzutka aplikacji

Pozostało nam teraz tylko wrzucić naszą aplikację na serwer znajdujący się w kontenerze. Musimy do tego wykorzystać komendę kopiującą docker cp. Dodatkowo dzięki wcześniej użytej komendzie docker container ls poznaliśmy nazwę naszego kontenera, crazy_montalcini, która będzie nam również niezbędna.

1
$ docker cp java-docker-app/target/java-docker-app.war crazy_montalcini:/usr/local/tomcat/webapps/

Zgodnie z dokumentacją docker cp [OPTIONS] SRC_PATH|- CONTAINER:DEST_PATH pierwszym argumentem jest plik źródłowy, potem podajemy nazwę naszego kontenera (lub id) i po dwukropku miejsce gdzie chcemy skopiować plik WAR. Tomcat domyślnie przechowuje aplikacje w folderze webapps. Jeżeli wrzucimy tam aplikacje w postaci pliku WAR to zostanie ona automatycznie rozpakowana i uruchomiona. Sprawdźmy czy tak faktycznie się stało.

1
2
3
4
5
6
7
$ docker cp java-docker-war/target/java-docker-war.war crazy_montalcini:/usr/local/tomcat/webapps/
$ docker exec -it crazy_montalcini bash
root@179947e64f49:/usr/local/tomcat# cd webapps
root@179947e64f49:/usr/local/tomcat/webapps# ls -l
total 16948
drwxr-x--- 5 root root     4096 Jun 24 16:09 java-docker-war
-rwxr-xr-x 1 root root 17347185 Jun 24 13:47 java-docker-war.war

Odpaliliśmy bash’a na naszym kontenerze, weszliśmy w odpowiedni folder i faktycznie znajduje się tam nasz plik WAR wraz z jego rozpakowaną wersją. Teraz możemy wejść w przeglądarce na adres http://localhost:8080/java-docker-war/welcome i naszym oczom ukaże się napis Welcome from WAR Java app.

Odświeżone podejście - packaging JAR

Sprawdźmy jak sytuacja ma się w przypadku pakowania aplikacji do pliku JAR. Zacznijmy jeszcze raz od utworzenia projektu w Spring’u.

Tworzenie aplikacji Spring Boot w opaciu o JAR

Tworzymy kontroler, dodajemy <finalName>${artifactId}</finalName> i korzystamy z komend Maven’a.

1
2
3
4
5
6
7
8
9
10
11
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
class JarController {

  @GetMapping("/welcome")
  String welcome() {
    return "Welcome from JAR Java app";
  }
}

Wygenerowany plik JAR

Teraz zamiast obrazu Tomcat’a wystarczy nam sam obraz zawierający Javę w wersji co najmniej 11. Jest to spowodowane tym, że Spring Boot dostarcza nam wbudowanego Tomcat’a podczas pakowania aplikacji do JAR. Dlatego jeszcze raz wchodzimy na hub.docker.com, wyszukujemy obraz openjdk i pobieramy go poprzez terminal.

Poszukiwanie obrazu OpenJDK

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ docker run -d -p 9090:8080 openjdk
Unable to find image 'openjdk:latest' locally
latest: Pulling from library/openjdk
5a581c13a8b9: Already exists
26cd02acd9c2: Already exists
66727af51578: Already exists
Digest: sha256:05eee0694a2ecfc3e94d29d420bd8703fa9dcc64755962e267fd5dfc22f23664
Status: Downloaded newer image for openjdk:latest
a42fa95b8c967bb8f596bdbdc17e4c933ed4d5f12a5326e782c0fed1bb10c174

$ docker container ls
CONTAINER ID   IMAGE     COMMAND             CREATED             STATUS             PORTS                                       NAMES
179947e64f49   tomcat    "catalina.sh run"   About an hour ago   Up About an hour   0.0.0.0:8080->8080/tcp, :::8080->8080/tcp   crazy_montalcini

$ docker container ls -a
CONTAINER ID   IMAGE     COMMAND             CREATED             STATUS                      PORTS                                       NAMES
a42fa95b8c96   openjdk   "jshell"            20 seconds ago      Exited (0) 15 seconds ago                                               nice_satoshi
179947e64f49   tomcat    "catalina.sh run"   About an hour ago   Up About an hour            0.0.0.0:8080->8080/tcp, :::8080->8080/tcp   crazy_montalcini

Coś nie zadziałało…

Obraz się ściągnął, ale nasz nowy kontener tylko na chwilę się uruchomił i od razu się wyłączył. Dopiero po wpisaniu docker container ls -a dowiadujemy się o tym. W tym wypadku musimy utworzyć plik Dockerfile, w którym przekażemy instrukcje o tym, aby nasza aplikacja została skopiowana do odpowiedniego folderu i uruchomiona. Tworzymy, więc wcześniej wspomniany plik i w nim wpisujemy następujące linijki.

FROM openjdk

COPY java-docker-jar/target/java-docker-jar.jar /deployments/

CMD java -jar /deployments/java-docker-jar.jar

Wydaliśmy tutaj komendy, aby pobrać obraz openjdk, skopiować naszą aplikację i ją uruchomić używając polecenia java -jar. Teraz wywołujemy polecenie docker image build -t java-docker-jar ., aby utworzyć nasz docelowy obraz wraz z aplikacją.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$ docker image build -t java-docker-jar .
[+] Building 5.0s (7/7) FINISHED
 => [internal] load build definition from Dockerfile                                                               0.8s
 => => transferring dockerfile: 167B                                                                               0.0s
 => [internal] load .dockerignore                                                                                  1.2s
 => => transferring context: 2B                                                                                    0.0s
 => [internal] load metadata for docker.io/library/openjdk:latest                                                  0.0s
 => [internal] load build context                                                                                  0.6s
 => => transferring context: 17.33MB                                                                               0.2s
 => CACHED [1/2] FROM docker.io/library/openjdk                                                                    0.0s
 => [2/2] COPY  java-docker-jar/target/java-docker-jar.jar /deployments/                                           2.9s
 => exporting to image                                                                                             0.1s
 => => exporting layers                                                                                            0.1s
 => => writing image sha256:375f3a6166e182a171b5a4763643c8381a71a030b43eeb46ceeea1320eef5d7e                       0.0s
 => => naming to docker.io/library/java-docker-jar                                                                 0.0s

$ docker image ls
REPOSITORY        TAG       IMAGE ID       CREATED         SIZE
java-docker-jar   latest    375f3a6166e1   8 seconds ago   484MB
tomcat            latest    6654503f1940   12 hours ago    667MB
openjdk           latest    de085dce79ff   3 weeks ago     467MB

W ten o to sposób utworzyliśmy nasz pierwszy obraz Docker’owy! Teraz musimy go tylko uruchomić.

1
2
3
4
5
6
7
$ docker run -d -p 9090:8080 java-docker-jar
bc9c6ae257e28a422920de3fe64de543a162c8102e9a5c77925945663126eafe

$ docker container ls
CONTAINER ID   IMAGE             COMMAND                  CREATED         STATUS         PORTS                                       NAMES
bc9c6ae257e2   java-docker-jar   "/bin/sh -c 'java -j…"   9 seconds ago   Up 7 seconds   0.0.0.0:9090->8080/tcp, :::9090->8080/tcp   frosty_golick
179947e64f49   tomcat            "catalina.sh run"        2 hours ago     Up 2 hours     0.0.0.0:8080->8080/tcp, :::8080->8080/tcp   crazy_montalcini 

Nowy kontener działa jak należy na porcie 9090. Wystarczy tylko wpisać http://localhost:9090/welcome w przeglądarkę i cieszyć się widokiem naszej aplikacji wyświetlającej Welcome from JAR Java app.

Podsumowanie

W dosyć krótki i szybki sposób chciałem Cię wprowadzić w zagadnienie Docker’a i uruchamiania na nim aplikacji Spring’owych spakowanych w JAR lub WAR. Uważam, że taka wiedza jest dobra na początek, aby sprawniej tworzyć aplikacje biznesowe lub swoje własne. Mam nadzieję, że niczego we wpisie nie pominąłem lecz jeżeli będziesz mieć jakieś pytania to zachęcam do ich zadawania. Z chęcią na nie odpowiem! 🙂