8. Criando e gerenciando imagens

8.1. Agora eu quero criar minha imagem, posso?

Claro que pode!

E mais, vamos aprender de duas formas simples e intuitivas.

Uma das coisas mais interessantes do Docker é a possibilidade de usar imagens criadas por outras pessoas ao redor do mundo através de algum registry como o Docker Hub. Isso agiliza muito a sua vida, ainda mais quando você precisa apenas testar uma determinada tecnologia. O POC (Proof of Concept -- em português, prova de conceito) se torna muito mais ágil, fazendo com que você consiga testar diversas ferramentas no mesmo tempo em que levaria para testar somente uma sem o Docker.

Entretanto, em determinados momentos precisamos criar a nossa própria imagem do zero, ou então modificar uma imagem criada por terceiros e salvar essas alterações em uma nova imagem.

Agora vamos ver os dois casos: como montar uma distribuição praticamente do zero utilizando somente instruções através do dockerfile e outra realizando modificações em uma imagem já existente e salvando em uma imagem nova.

8.2. Vamos começar do começo então, dockerfile!

Vamos montar a nossa primeira imagem utilizando como roteiro de criação um dockerfile. Você verá o quanto é simples a criação de um dockerfile bem completo e prático. :)

Para começar, vamos criar um diretório chamado "/root/Dockerfiles".

# mkdir /root/Dockerfiles

Agora começaremos a criação do nosso dockerfile, nosso mapa de criação da imagem. Para que possamos organizá-lo melhor, vamos criar um diretório chamado "apache", onde guardaremos esse nosso primeiro exemplo:

# cd /root/Dockerfiles/
# mkdir apache

Por enquanto, vamos apenas criar um arquivo chamado "Dockerfile" e adicionar o conteúdo conforme exemplo a seguir:

# cd apache
# vim Dockerfile
FROM debian:10

RUN apt-get update && apt-get install -y apache2 && apt-get clean

ENV APACHE_LOCK_DIR="/var/lock"

ENV APACHE_PID_FILE="/var/run/apache2.pid"

ENV APACHE_RUN_USER="www-data"

ENV APACHE_RUN_GROUP="www-data"

ENV APACHE_LOG_DIR="/var/log/apache2"

LABEL description="Webserver"

VOLUME /var/www/html/

EXPOSE 80

Muito bom! Agora que você já adicionou as informações conforme o exemplo, vamos entender cada seção utilizada nesse nosso primeiro dockerfile:

  • FROM -- Indica a imagem a servir como base.

  • RUN -- Lista de comandos que deseja executar na criação da imagem.

  • ENV -- Define variáveis de ambiente.

  • LABEL -- Adiciona metadata à imagem, como descrição, versão, etc.

  • VOLUME -- Define um volume a ser montado no container.

Após a criação do arquivo, vamos buildar (construir a nossa imagem) da seguinte forma:

# docker build .

Lembre-se: você deverá estar no diretório onde está o seu dockerfile.

Todos os passos que definimos em nosso dockerfile serão realizados, como a instalação dos pacotes solicitados e todas as demais tarefas.

Successfully built 53de2cee9e71

Muito bem! Como podemos notar na última linha da saída do "docker build", a imagem foi criada com sucesso! :D

Vamos executar o "docker image ls" para ver se está tudo certo com a nossa primeira imagem!

root@linuxtips:~/Dockerfile/apache# docker image ls
REPOSITORY       TAG     IMAGE ID      CREATED         SIZE
<none>           <none>  53de2cee9e71  2 minutes ago   193.4 MB

A nossa imagem foi criada! Porém, temos um problema. :/

A imagem foi criada e está totalmente funcional, mas, quando a buildamos, não passamos o parâmetro "-t", que é o responsável por adicionar uma tag ("nome:versão") à imagem.

Vamos executar novamente o build, porém passando o parâmetro '-t', conforme o exemplo a seguir:

# docker build -t linuxtips/apache:1.0 .

Agora vamos ver se realmente a imagem foi criada, adicionando um nome e uma versão a ela:

root@linuxtips:~/Dockerfile/apache# docker image ls
REPOSITORY         TAG   IMAGE ID      CREATED        SIZE
linuxtips/apache   1.0   53de2cee9e71  5 minutes ago  193.4 MB

Maravilha! Funcionou conforme esperávamos!

Vamos executar um container utilizando nossa imagem como base:

# docker container run -ti linuxtips/apache:1.0

Agora já estamos no container. Vamos verificar se o Apache2 está em execução. Se ainda não estiver, vamos iniciá-lo e verificar se a porta 80 está "LISTEN".

root@70dd36fe2d3b:/# ps -ef
UID          PID    PPID  C STIME TTY          TIME CMD
root           1       0  1 21:33 ?        00:00:00 /bin/bash
root           6       1  0 21:33 ?        00:00:00 ps -ef

root@70dd36fe2d3b:/# /etc/init.d/apache2 start
[....] Starting Apache httpd web server: apache2AH00558: apache2: Could not reliably determine the server's fully qualified domain name, using 172.17.0.4. Set the 'ServerName' directive globally to suppress this message
. ok 

root@70dd36fe2d3b:/# ps -ef
UID          PID    PPID  C STIME TTY          TIME CMD
root           1       0  0 21:33 ?        00:00:00 /bin/bash
root          30       1  0 21:33 ?        00:00:00 /usr/sbin/apache2 -k start
www-data      33      30  0 21:33 ?        00:00:00 /usr/sbin/apache2 -k start
www-data      34      30  0 21:33 ?        00:00:00 /usr/sbin/apache2 -k start
root         109       1  0 21:33 ?        00:00:00 ps -ef

root@70dd36fe2d3b:/# ss -atn
State      Recv-Q Send-Q     Local Address:Port    Peer Address:Port
LISTEN          0    128            :::80                :::*

root@70dd36fe2d3b:/# ip addr show eth0
50: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
        link/ether 02:42:ac:11:00:04 brd ff:ff:ff:ff:ff:ff
        inet 172.17.0.4/16 scope global eth0
             valid_lft forever preferred_lft forever
        inet6 fe80::42:acff:fe11:4/64 scope link
             valid_lft forever preferred_lft forever

root@70dd36fe2d3b:/#

No código anterior é possível observar o IP do container na saída do "ip addr". Vamos testar a comunicação com o container a partir do host.

No host, digite:

# curl <IP DO CONTAINER>

O "curl" retornou a página de boas-vindas do Apache2, ou seja, tudo está funcionando muito bem e o Apache2, respondendo conforme esperado!

Maaaaaasssss, não é interessante que eu tenha que entrar no container para subir o meu processo do Apache. Todo container deve executar seu processo em primeiro plano e esse processo deve subir de forma automática e não com um serumaninho acessando o container e subindo o serviço. Vimos antes somente como primeiro exemplo, agora vamos aperfeiçoá-lo e deixá-lo como deve estar! :D

A primeira coisa é deixar o nosso dockerfile como segue:

# vim Dockerfile
FROM debian:10

RUN apt-get update && apt-get install -y apache2 && apt-get clean

ENV APACHE_LOCK_DIR="/var/lock"

ENV APACHE_PID_FILE="/var/run/apache2/apache2.pid"

ENV APACHE_RUN_USER="www-data"

ENV APACHE_RUN_DIR="/var/run/apache2"

ENV APACHE_RUN_GROUP="www-data"

ENV APACHE_LOG_DIR="/var/log/apache2"

LABEL description="Webserver"

VOLUME /var/www/html/

EXPOSE 80

ENTRYPOINT ["/usr/sbin/apachectl"]

CMD ["-D", "FOREGROUND"]

Perceba que agora nós adicionamos mais duas opções: o ENTRYPOINT e o CMD!

Ficou curioso sobre o que eles fazem? Então 'bora aprender muito mais opções possíveis de serem adicionadas em um dockerfile!

8.3. Bora aprender um pouco mais sobre dockerfile?

Vamos agora aprender um pouco mais sobre as opções que podemos utilizar quando estamos criando um dockerfile:

  • ADD -- Copia novos arquivos, diretórios, arquivos TAR ou arquivos remotos e os adiciona ao filesystem do container.

  • CMD -- Executa um comando. Diferentemente do RUN, que executa o comando no momento em que está "buildando" a imagem, o CMD irá fazê-lo somente quando o container é iniciado.

  • LABEL -- Adiciona metadados à imagem, como versão, descrição e fabricante.

  • COPY -- Copia novos arquivos e diretórios e os adiciona ao filesystem do container.

  • ENTRYPOINT -- Permite que você configure um container para rodar um executável. Quando esse executável for finalizado, o container também será.

  • ENV -- Informa variáveis de ambiente ao container.

  • EXPOSE -- Informa qual porta o container estará ouvindo.

  • FROM -- Indica qual imagem será utilizada como base. Ela precisa ser a primeira linha do dockerfile.

  • MAINTAINER -- Autor da imagem.

  • RUN -- Executa qualquer comando em uma nova camada no topo da imagem e "commita" as alterações. Essas alterações você poderá utilizar nas próximas instruções de seu dockerfile.

  • USER -- Determina qual usuário será utilizado na imagem. Por default é o root.

  • VOLUME -- Permite a criação de um ponto de montagem no container.

  • WORKDIR -- Responsável por mudar do diretório "/" (raiz) para o especificado nele.

Um detalhe superimportante de mencionar é que quando estamos trabalhando com o ENTRYPOINT e o CMD dentro do mesmo dockerfile, o CMD somente aceita parâmetros do ENTRYPOINT, conforme nosso exemplo do dockerfile anterior:

ENTRYPOINT ["/usr/sbin/apachectl"]

CMD ["-D", "FOREGROUND"]

Onde:

  • "/usr/sbin/apachectl" -- Esse é o comando.

  • "-D", "FOREGROUND" -- Esse é o argumento, o parâmetro.

No shell, por exemplo, a execução ficaria assim:

# /usr/sbin/apachectl -D FOREGROUND

Ou seja, assim você está iniciando o Apache passando a instrução para que ele seja iniciado em primeiro plano, como deve ser. :D

Para maiores detalhes sobre como criar imagens, veja essa apresentação criada pelo Jeferson: https://www.slideshare.net/jfnredes/images-deep-dive.

8.4. Multi-stage

Um importante e recente recurso adicionado ao dockerfile visa facilitar a vida de quem pretende criar imagens de containers de forma efetiva. Esse cara é o multi-stage!

O multi-stage nada mais é do que a possibilidade de você criar uma espécie de pipeline em nosso dockerfile, podendo inclusive ter duas entradas FROM.

Esse recurso é muito utilizado quando queremos, por exemplo, compilar a nossa aplicação em um container e executá-la, porém não queremos ter aquela quantidade enorme de pacotes instalados em nossos containers necessários sempre quando se quer compilar códigos de alguma linguagem, como C, Java ou Golang.

Vamos a um exemplo para que possamos entender melhor como isso funciona!

Para isso, preparei uma app escrita em Golang superavançada para o nosso teste:

# vim goapp.go
package main

import "fmt"

func main() {

    fmt.Println("GIROPOPS STRIGUS GIRUS - LINUXTIPS")

}

Achou que seria algo avançado? Impossível, fomos nós que fizemos. :D

Bem, agora vamos criar um dockerfile para criar a nossa imagem e assim executar a nossa app.

# vim Dockerfile
FROM golang

WORKDIR /app

ADD . /app

RUN go mod init goapp && go build -o goapp

ENTRYPOINT ./goapp

Pronto! Agora vamos realizar o build.

# docker build -t goapp:1.0 .

Listando a nossa imagem:

# docker image ls | grep goapp
goapp    1.0    50451808b384    11 seconds ago      781MB

Agora vamos executá-la e ver a nossa fantástica app em execução:

# docker container run -ti goapp:1.0
GIROPOPS STRIGUS GIRUS -- LINUXTIPS

Pronto! Nossa app e nossa imagem estão funcionando! Sucesso!

Porém, podemos melhorar muita coisa se começarmos a utilizar o nosso poderoso recurso, o multi-stage!

Vamos refazer o nosso dockerfile utilizando o multi-stage, entender como ele funciona e a diferença entre as duas imagens.

Vamos deixar nosso dockerfile dessa maneira:

# vim Dockerfile
FROM golang AS buildando

ADD . /src

WORKDIR /src

RUN go mod init goapp && go build -o goapp

FROM alpine:3.1

WORKDIR /app

COPY --from=buildando /src/goapp /app

ENTRYPOINT ./goapp

Perceba que agora nós temos duas entradas FROM, o que não era possível antes do multi-stage. Mas por que isso?

O que está acontecendo é que agora temos o dockerfile dividido em duas seções. Cada entrada FROM define o início de um bloco, uma etapa.

Então, em nosso primeiro bloco temos:

  • FROM golang AS buildando -- Estamos utilizando a imagem do Golang para criação da imagem de container, e aqui estamos apelidando esse bloco como "buildando".

  • ADD . /src -- Adicionando o código de nossa app dentro do container no diretório "/src".

  • WORKDIR /src -- Definindo que o diretório de trabalho é o "/src", ou seja, quando o container iniciar, estaremos nesse diretório.

  • RUN go build -o goapp -- Vamos executar o build de nossa app Golang.

Já no segundo bloco temos o seguinte:

  • FROM alpine:3.1 -- Iniciando o segundo bloco e utilizando a imagem do Alpine para criação da imagem de container.

  • WORKDIR /app -- Definindo que o diretório de trabalho é o "/app", ou seja, quando o container iniciar, estaremos nesse diretório.

  • COPY --from=buildando /src/goapp /app -- Aqui está a mágica: vamos copiar do bloco chamado "buildando" um arquivo dentro de "/src/goapp" para o diretório "/app" do container que estamos tratando nesse bloco, ou seja, copiamos o binário que foi compilado no bloco anterior e o trouxemos para esse.

  • ENTRYPOINT ./goapp -- Aqui vamos executar a nossa sensacional app. :)

Agora que já entendemos todas as linhas do nosso novo dockerfile, 'bora realizar o build dele.

# docker build -t goapp_multistage:1.0 .

Vamos executar a nossa imagem para ver se está tudo funcionando:

# docker container run -ti goapp_multistage:1.0
GIROPOPS STRIGUS GIRUS - LINUXTIPS

Será que existe diferença de tamanho entre elas? Vamos conferir:

# docker image ls | grep goapp
goapp_multistage  1.0    dfe57485b7f0    22 seconds ago    7.07MB
goapp             1.0    50451808b384    15 minutes ago     781MB

A diferença de tamanho é brutal, pois em nossa primeira imagem precisamos ter um monte de pacotes para que o build da app Golang ocorra. Já em nossa segunda imagem também utilizamos a imagem do Golang e todos os seus pacotes para buildar a nossa app, porém descartamos a primeira imagem e somente copiamos o binário para o segundo bloco, onde estamos utilizando a imagem do Alpine, que é superenxuta.

Ou seja, utilizamos o primeiro bloco para compilar a nossa app e o segundo bloco somente para executá-la. Simples assim, simples como voar! :D

8.5. Vamos customizar uma imagem base agora?

Vamos agora criar uma nova imagem, porém sem utilizar o dockerfile. Vamos executar um container com uma imagem base, realizar as modificações que desejarmos e depois salvar esse container como uma nova imagem!

Simples, rápido e fácil!

Bem, primeiro precisamos criar um container. Vamos dessa vez utilizar um container Debian, somente para variar. :D

root@linuxtips:~# docker container run -ti debian:8 /bin/bash
root@0b7e6f606aae:/#

Agora vamos fazer as alterações que desejamos. Vamos fazer o mesmo que fizemos quando montamos nossa primeira imagem com o dockerfile, ou seja, fazer a instalação do Apache2. :D

root@0b7e6f606aae:/# apt-get update && apt-get install -y apache2 && apt-get clean

Agora que já instalamos o Apache2, vamos sair do container para que possamos commitar a nossa imagem com base nesse container em execução.

Lembre-se de que para sair do container e deixá-lo ainda em execução é necessário pressionar Ctrl + p + q. ;)

# docker container ls
# docker commit -m "meu container" CONTAINERID
# docker image ls
REPOSITORY   TAG     IMAGE ID       CREATED         SIZE
<none>       <none>  fd131aedd43a   4 seconds ago   193.4 MB

Repare que nossa imagem ficou com o "\" em seu nome e "TAG". Para que possamos ajustar e dar um nome e uma versão à nossa imagem, vamos usar o comando "docker tag", conforme mostramos a seguir:

# docker tag IMAGEID linuxtips/apache_2:1.0
REPOSITORY          TAG   IMAGE ID       CREATED          SIZE
linuxtips/apache_2  1.0   fd131aedd43a   2 minutes ago    193.4 MB

Agora sim!!! Temos a nossa imagem criada e nome e versão especificados.

Vamos iniciar um container utilizando a imagem que acabamos de criar:

# docker container run -ti linuxtips/apache_2:1.0 /bin/bash

Vamos subir o Apache2 e testar a comunicação do container:

root@57094ec894ce:/# ps -ef
UID   PID PPID C  STIME TTY      TIME  CMD
root    1    0 0  21:48 ?    00:00:00  /bin/bash
root    6    1 0  21:48 ?    00:00:00  ps -ef

root@57094ec894ce:/# /etc/init.d/apache2 start
[....] Starting web server: apache2AH00558: apache2: Could notreliably determine the server's fully qualified domain name, using 172.17.0.6. Set the 'ServerName' directive globally to suppress this message
. ok

root@70dd36fe2d3b:/# ps -ef
UID          PID    PPID  C STIME TTY          TIME CMD
root           1       0  0 21:43 ?        00:00:00 /bin/bash
root          30       1  0 21:44 ?        00:00:00 /usr/sbin/apache2 -k start
www-data      33      30  0 21:44 ?        00:00:00 /usr/sbin/apache2 -k start
www-data      34      30  0 21:44 ?        00:00:00 /usr/sbin/apache2 -k start
root         111       1  0 21:44 ?        00:00:00 ps -ef

root@70dd36fe2d3b:/# ss -atn
State     Recv-Q   Send-Q       Local Address:Port        Peer Address:Port
LISTEN         0      128             :::80                      :::*

root@57094ec894ce:/# ip addr show eth0
54: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
       link/ether 02:42:ac:11:00:06 brd ff:ff:ff:ff:ff:ff
       inet 172.17.0.6/16 scope global eth0
          valid_lft forever preferred_lft forever
       inet6 fe80::42:acff:fe11:6/64 scope link
          valid_lft forever preferred_lft forever

Boaaa! Agora já temos o Apache2 em execução. Vamos sair do container e testar a comunicação com o Apache2 a partir do host:

# curl <Container IP>

Ele retornará a página de boas-vindas do Apache2! Tudo funcionando conforme esperado!

results matching ""

    No results matching ""