Hello Java from Docker

Partendo dal banale Hello World vedremo come integrare all’interno di un container Docker il download di un nostro progetto d’esempio da GitHub, compilarlo ed avviarlo.

Senza la presunzione di riassumerne tutto il funzionamento in poche righe, possiamo spendere due parole sul suo funzionamento. Docker permette la definizione, composizione e in qualche modo anche la gestione dei Container. Questa parola è molto usata nell’informatica e in questo caso possiamo associare a Container il significato di versione light di un sistema operativo dove poter far girare i nostri servizi, essendo sicuro che questo non vada in conflitto con altre cose. Il parallelo che si usa per capire il funzionamento dei Container è quello con le Virtual Machine, riepilogato nella seguente immagine

© Docker

© Docker

Quando vogliamo mettere le nostre applicazioni su una Virtual Machine questa ha un suo sistema operativo dedicato, quindi dovendo gestire diverse applicazioni su VM simili c’è uno spreco di risorse gestite. I Container sono un’astrazione che permette di gestire i processi isolati ma che condivisono il kernel del sistema operativo. Per questo motivo i Container sono più veloci e snelli rispetto alle classiche VM. Docker, sfruttando il concetto di Container, ci permette di definirne uno in un modo molto semplice e che può essere estremamente utile nelle fasi di sviluppo, test e anche produzione. L’architettura di Docker può essere rappresentata nel seguente diagramma dove abbiamo il Docker client che rappresenta il tool che utilizziamo da riga di comando. Questo si collega al demone Docker che a sua volta gestisce i Docker container.

Prima di tutto dobbiamo avere Docker installato sul nostro sistema, se così non fosse vi rimando alla guida ufficiale sul sito di Docker che vi spiega come installarlo nei diversi sistemi operativi. Per verificare che l’installazione sia stata effettuata correttamente possiamo lanciare il seguente comando

federico@work:~$ sudo docker run hello-world

Unable to find image 'hello-world:latest' locally

latest: Pulling from library/hello-world

78445dd45222: Pull complete

Digest: sha256:c5515758d4c5e1e838e9cd307f6c6a0d620b5e07e6f927b07d05f6d12a1ac8d7

Status: Downloaded newer image for hello-world:latest


Hello from Docker!

This message shows that your installation appears to be working correctly.


To generate this message, Docker took the following steps:

    The Docker client contacted the Docker daemon.
    The Docker daemon pulled the "hello-world" image from the Docker Hub.
    The Docker daemon created a new container from that image which runs the

    executable that produces the output you are currently reading.

    The Docker daemon streamed that output to the Docker client, which sent it

    to your terminal.


To try something more ambitious, you can run an Ubuntu container with:

$ docker run -it ubuntu bash


Share images, automate workflows, and more with a free Docker ID:

https://cloud.docker.com/


For more examples and ideas, visit:

https://docs.docker.com/engine/userguide/

 

In questo simpatico esempio di Hello World possiamo trovare già le informazioni di cosa è successo dietro alle quinte. Il client Docker ha comunicato al demone di far partire l’immagine hello-world. Localmente non avevamo questa immagine e quindi il demone si è collegato a Docker Hub, ha scaricato l’immagine localmente e ha creato un nuovo container a partire da questa immagine facendolo partire. Il container nella sua definizione interna ha un eseguibile che crea l’output appena riportato. Se volessimo creare anche noi una esempio simile a quello riportato il file possiamo creare la seguente immagine

 

FROM ubuntu:latest

COPY hello.txt /

RUN cat hello.txt

 

Con questi tre comandi all’interno del file Dockerfile, che è il file principale di configurazione per un’immagine Docker, stiamo dicendo le seguenti cose

1) Parti dall’immagine di ubuntu, ultima versione

2) Copia il file hello.txt nella root dell’immagine

3) Esegui il comando cat hello.txt

Passiamo quindi ad effettuare il build per verificarne il funzionamento.

federico@work:~$ sudo docker build -t hello-world-mio .

Sending build context to Docker daemon 3.584 kB

Step 1/3 : FROM ubuntu:latest

---> 0ef2e08ed3fa

Step 2/3 : COPY readme.txt /

---> 7e7b8d9e9872

Removing intermediate container b64abf233e7e

Step 3/3 : RUN cat readme.txt

---> Running in fe0e33c1c2c8

'##::::'##:'########:'##:::::::'##::::::::'#######::

##:::: ##: ##.....:: ##::::::: ##:::::::'##.... ##:

##:::: ##: ##::::::: ##::::::: ##::::::: ##:::: ##:

#########: ######::: ##::::::: ##::::::: ##:::: ##:

##.... ##: ##...:::: ##::::::: ##::::::: ##:::: ##:

##:::: ##: ##::::::: ##::::::: ##::::::: ##:::: ##:

##:::: ##: ########: ########: ########:. #######::

..:::::..::........::........::........:::.......:::

'##:::::'##::'#######::'########::'##:::::::'########::

##:'##: ##:'##.... ##: ##.... ##: ##::::::: ##.... ##:

##: ##: ##: ##:::: ##: ##:::: ##: ##::::::: ##:::: ##:

##: ##: ##: ##:::: ##: ########:: ##::::::: ##:::: ##:

##: ##: ##: ##:::: ##: ##.. ##::: ##::::::: ##:::: ##:

##: ##: ##: ##:::: ##: ##::. ##:: ##::::::: ##:::: ##:

. ###. ###::. #######:: ##:::. ##: ########: ########::

:...::...::::.......:::..:::::..::........::........:::

---> 1e1c45f7ff78

Removing intermediate container fe0e33c1c2c8

Successfully built 1e1c45f7ff78

 

Ogni comando riportato nel file di build crea un container intermedio dove viene eseguito il comando corrente. L’immagine finale verrà salvata in locale, come è possibile vedere dall’elenco delle immagini

federico@work:~$ sudo docker images

REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE

hello-world-mio     latest              1e1c45f7ff78        7 minutes ago       130 MB

 

Hello World Java

Passiamo ora ad un Hello World più vicino ai nostri interessi. Come immagine base in questo caso utilizzeremo quella ufficiale java, che ovviamente ha al suo interno già installato Java. Il Dockerfile sarà quindi il seguente

FROM java:8

COPY HelloWorld.java /

RUN javac HelloWorld.java

RUN java HelloWorld

 

In questo caso possiamo vedere l’output del nostro Hello World semplicemente lanciando la build

 

federico@work:~/Docker/hello-world-java$ sudo docker build --no-cache=true -t hello-world-java .

Sending build context to Docker daemon 3.072 kB

Step 1/4 : FROM java:8

---> d23bdf5b1b1b

Step 2/4 : COPY HelloWorld.java /

---> 19a8fcd746db

Removing intermediate container 03a2f1d4788e

Step 3/4 : RUN javac HelloWorld.java

---> Running in 1e7cdba1287f

---> 66561106ada7

Removing intermediate container 1e7cdba1287f

Step 4/4 : RUN java HelloWorld

---> Running in 454dc2efe378

Hello World da dentro un container

---> a5e078df59b4

Removing intermediate container 454dc2efe378

Successfully built a5e078df59b4

Ovviamente potevamo partire da un immagine base e installare Java da soli, ma avendo a disposizione delle immagini ufficiali rilasciate dai diversi produttori software talvolta può essere conveniente sfruttarle senza creare Dockerfile lunghi.

 

Hello World Maven

Dopo aver provato l’ebbrezza di compilare un Hello World dentro il nostro container dobbiamo provare a fare la build di un progetto Maven e lanciarlo. Prima di tutto dobbiamo definire un semplice progetto che ci permette di lanciare un webserver. L’unica classe di questo progetto è quella che trovate riportata di seguito

package com.javastaff;

import java.io.IOException;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpServer;

public class ExampleWebserver {

   public static void main(String[] args) throws Exception {
       HttpServer server = HttpServer.create(
          new InetSocketAddress(8080), 0);
       server.createContext("/", new MyHandler());
       server.start();
    }


    static class MyHandler implements HttpHandler {
       @Override
       public void handle(HttpExchange t) throws IOException {
          StringBuilder responseBuilder=new StringBuilder();
          responseBuilder
             .append("<html><head><title>ExampleWebserver</title></head><body>");
          responseBuilder.append("<h3>ExampleWebserver</h3>")
                         .append("Tu hai richiesto l'URL ")
                         .append(t.getRequestURI())
                         .append(" ma io non so ancora come poterlo gestire :(");

           t.sendResponseHeaders(200, responseBuilder.toString().length());
           OutputStream os = t.getResponseBody();
           os.write(responseBuilder.toString().getBytes());
           os.close();
       }
    }
}

 

Non soffermandosi troppo sull’utilità di questa classe, vediamo che lanciandola possiamo avere un inutile webserver sulla pagina 8080. Questa classe, insieme al progetto Maven che crea il relativo jar, è disponibile su github al repository https://github.com/fpaparoni/ExampleWebserver. Ora definiremo un Dockerfile che scaricherà il progetto da github, lo compilerà e lancerà

 

FROM maven:latest

EXPOSE 8080

RUN apt-get install git

RUN git clone https://github.com/fpaparoni/ExampleWebserver.git

WORKDIR ExampleWebserver/

RUN mvn clean package

WORKDIR target/

CMD ["java","-jar","example-webserver-1.0-SNAPSHOT.jar"]

 

L’immagine di partenza che utilizziamo è quella ufficiale rilasciata da Maven, che ovviamente ha installato Maven e Java. Successivamente con il comando EXPOSE mettiamo in ascolto la porta 8080 e scarichiamo il progetto dal repository git. Passiamo quindi alla directory appena creata con il comando WORKDIR e creiamo il JAR con il classico comando di Maven. Lanciamo quindi l’eseguibile Java dopo essere entrati nella directory target. Passiamo quindi al build della nostra nuova immagine

 

sudo docker build --no-cache=true -t hello-world-maven .

 

e al successivo run del nuovo container dove con il parametro -p8080:8080 stiamo mappando la porta 8080 del container con la porta 8080 del nostro sistema operativo

 

sudo docker run -p8080:8080 -t hello-world-maven

 

Collegandoci all’indirizzo localhost:8080 troveremo in ascolto il nostro webserver d’esempio lanciato dal container Docker. A questo punto dell’articolo vi sarebbe dovuto già venire in mente la domanda “Si ma ho lanciato questi container, ma poi come faccio a vederli? come faccio a fermarli??”. Con il comando docker ps -a possiamo vedere tutti i container presenti sul nostro sistema e in seguito possiamo stopparli con il comando docker stop <CONTAINER-ID> o addirittura cancellarli con il comando docker rm <CONTAINER-ID>.

 

Docker Compose e Spring Boot

Passiamo ora ad un esempio che permette di vedere qualcosa di più complesso del semplice hello world. L’applicazione che vogliamo far girare un’applicazione è realizzata con Spring Boot, un esempio di crud che trovate all’indirizzo https://github.com/fpaparoni/spring-boot-crud, che utilizza un database MySQL per memorizzare le informazioni. Abbiamo quindi bisogno di avviare diversi processi che dialogano tra di loro, quindi possiamo utilizzare Docker Compose, tool che serve per definire e lanciare applicazioni basate su molteplici container Docker. L’installazione di questo tool è semplicemente il download del binario per la vostra piattaforma, ma vi rimando comunque alla documentazione ufficiale.

Prima di addentrarci nella configurazione del file relativo a Docker Compose, dobbiamo definire le immagini relative al database e alla nostra applicazione. Partiamo con il database MySQL, dove rispetto all’immagine ufficiale andremo ad aggiungere uno script che verrà eseguito all’avvio dove verrà creato il database e la tabella che ci interessa

FROM mysql

MAINTAINER Federico Paparoni

ENV MYSQL_ROOT_PASSWORD=my-secret-pw

ADD script.sql /docker-entrypoint-initdb.d

EXPOSE 3306

e lanciamo la build

sudo docker build --no-cache=true -t custom-mysql .

 

Passiamo quindi all’applicazione Spring Boot. In questo caso il file di build scaricherà il progetto da git, lo compilerà, copierà un file di properties custom dove abbiamo inserito l’host MySQL a cui collegarsi ed infine avvierà l’applicazione in modalità debug, così potremo anche collegarci dal nostro IDE.

 

FROM maven:latest

MAINTAINER Federico Paparoni

EXPOSE 8080

EXPOSE 8000

RUN apt-get update

RUN apt-get -y install git

RUN git clone https://github.com/fpaparoni/spring-boot-crud.git

WORKDIR spring-boot-crud/

ADD application.properties src/main/resources/application.properties

RUN mvn clean package -DskipTests

WORKDIR target/

CMD ["java","-Xdebug","-Xrunjdwp:server=y,transport=dt_socket,address=8000,suspend=n","-jar","spring-boot-crud.jar"]

 

e anche per questa lanciamo la corrispondente build

 

sudo docker build --no-cache=true -t spring-boot-crud .

 

Ora che abbiamo definito queste due immagini passiamo alla definizione del file docker-compose.yml

 

version: '2.1'

services:

    custom-mysql:

       image: custom-mysql:latest

       ports:

               - "3306:3306"

       healthcheck:

           test: ["CMD", "mysqladmin" ,"ping", "-h", "localhost"]

           timeout: 20s

           retries: 10

    spring-boot-crud:

       image: spring-boot-crud:latest

       ports:

          - "8080:8080"

          - "8000:8000"

       links:

           - custom-mysql

       depends_on:

           custom-mysql:

               condition: service_healthy

 

In questo file definiamo una lista di servizi da lanciare, il primo dei quali è MySQL con l’immagine custom-mysql che abbiamo definito precedentemente. Per questa immagine specifichiamo la porta da esporre e un healthcheck che viene utilizzato per capire se il database è raggiungibile e utilizzabile. C’è poi la definizione del servizio con l’applicazione crud. In questo caso le porte esposte sono due, una per l’applicazione e l’altra per raggiungerla con il debug remoto. Definiamo inoltre un link tra questa immagine e il servizio custom-mysql, in questo modo il servizio spring-boot-crud potrà accedere al servizio database. Facciamo quindi partire Docker Compose lanciando il seguente comando dalla directory dove è presente il file che riporta la precedente composizione di servizi

 

sudo docker-compose up

 

Come è possibile vedere dai log, partirà prima MySQL e successivamente l’applicazione in quando abbiamo messo come condizione che il servizio MySQL sia raggiungibile ed utilizzabile. Per raggiungere l’applicazione basta andare sulla porta 8080 dal nostro browser, mentre invece se vogliamo agganciare il debug remoto basta configurarlo come riportato ad esempio nella seguente immagine dove all’interno di Eclipse viene configurato un server remoto dove collegarsi

 

Conclusioni

Potremmo continuare con molti altri esempi, visto che ovviamente gli scenari per utilizzare Docker sono tantissimi. Di sicuro per approfondire l’argomento è interessante vedere sia l’utilizzo di Docker da plugin Maven (ne esistono diversi) e Docker Swarm che permette di gestire un cluster di macchine virtuali. Tutti gli esempi sono scaricabili dal progetto Github riportato di seguito

 

Federico Paparoni

Looking for a right "about me"...

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *

Questo sito usa Akismet per ridurre lo spam. Scopri come i tuoi dati vengono elaborati.