Alô galera! Retomando nossos estudos sobre Docker, hoje apresentamos mais um post da série. Já vimos o conceito e a estrutura do docker, seus recursos de gerenciamento de armazenamento e redes, e mostramos seu funcionamento básico na prática, abordando desde a instalação até o deploy simples de aplicações containerizadas. Nesse post vamos abordar a criação de imagens.
Uma imagem, conforme abordamos no primeiro post, é composta por camadas. Mais especificamente, uma imagem é um conjunto de camadas empilhadas sobre uma camada base. Cada camada da imagem corresponde a uma ação. Podemos verificar as ações (comandos) que criaram cada camada de uma determinada imagem, observando a coluna CREATED BY da saída do comando docker history, conforme abaixo:
root@docker-host:~# docker history ubuntu:15.04
IMAGE CREATED CREATED BY SIZE COMMENT
d1b55fd07600 5 months ago /bin/sh -c #(nop) CMD ["/bin/bash"] 0 B
<missing> 5 months ago /bin/sh -c sed -i 's/^#\s*\(deb.*universe\)$/ 1.879 kB
<missing> 5 months ago /bin/sh -c echo '#!/bin/sh' > /usr/sbin/polic 701 B
<missing> 5 months ago /bin/sh -c #(nop) ADD file:3f4708cf445dc1b537 131.3 MB
A última camada da lista (de cima para baixo) é a base da imagem. As camadas adjacentes foram criadas por instruções que executaram comandos (echo, sed), ou que definiram uma configuração padrão na imagem (/bin/bash). Além disso, o tamanho das camadas varia de acordo com a instrução. Por exemplo, uma camada criada a partir de uma instrução que define uma configuração, será menor do que uma camada onde um arquivo foi criado ou um pacote instalado.
A criação de uma imagem personalizada consiste basicamente em acrescentar novas camadas à imagem base. Esse processo é feito usando containers.
Como abordamos no primeiro post, ao criar um container o docker adiciona uma camada ao topo da pilha de camadas da imagem. Essa camada, diferente das subjacentes, possui permissão de escrita, o que significa que podemos efetuar quaisquer modificações no container sem afetar a imagem. No entanto, essa não é uma camada permanente, ou seja, quando removemos o container ela é deletada. Isso significa que todas as alterações são perdidas.
O processo de construção de imagens consiste basicamente em adicionar (commit), de modo permanente, a camada de um container ao topo da pilha de camadas da imagem. Nesse caso, o conteúdo do container fará parte da imagem criada.
Os passos executadas no processo de criação de uma imagem são: (1) criar o container, (2) efetuar as alterações desejadas, (3) adicionar o container a imagem, e (4) remover o container. Esse processo pode ser feito de dois modos: manual ou automatizado. Vamos começar com o processo manual.
Antes de começarmos, no entanto, você pode criar uma conta no hub (http://hub.docker.com), caso ainda não possua uma, para armazenar suas imagens criadas nesse post. Lembre-se de alterar o nome das imagens criadas aqui para corresponder ao seu repositório.
Modo Manual
Nesse modo executamos todos os passos de forma manual. Já abordamos no post anterior a criação e manipulação de containers. O processo de adicionar a camada do container na imagem é feito usando o comando docker commit. A sintaxe desse comando é docker commit [OPTIONS] CONTAINER [REPOSITORY[:TAG]]. Podemos identificar o container pelo seu identificador (container id) ou pelo seu nome.
Vejamos esse modo na prática. Primeiro, vamos criar um container a partir da imagem do ubuntu 15.04, adicionando um arquivo simples:
root@docker-host:~# docker run -it ubuntu:15.04
root@782ffd0a38ea:/# echo "Ubuntu Modificado!" > /tmp/commit.txt
Em seguida, vamos construir uma imagem a partir desse container usando a forma mais simples do comando docker commit. Lembre-se de sair do container antes de executar o comando:
root@docker-host:~# docker commit 782ffd0a38ea cmotta2016/ubuntu-modificado:1.0 sha256:4f48cbb902400ac9f7b1c7a770e056b21fe73086deff7c71f614b81f30076566
OBS: Nesse post utilizaremos a TAG com o propósito de identificar a versão da nossa imagem.
Repare que após executar o comando o daemon exibe o identificador da imagem criada.
Crie um container sobre a nova imagem imprimindo no terminal o conteúdo do arquivo criado. Usamos a opção –rm para remover o container após a execução do comando especificado:
root@docker-host:~# docker run --rm cmotta2016/ubuntu-modificado:1.0 cat /tmp/commit.txt Ubuntu Modificado!
Agora, liste as camadas da imagem criada e repare no comando que criou a camada recém adicionada à imagem:
root@docker-host:~# docker history cmotta2016/ubuntu-modificado:1.0 IMAGE CREATED CREATED BY SIZE COMMENT 4f48cbb90240 2 minutes ago /bin/bash 19 B d1b55fd07600 5 months ago /bin/sh -c #(nop) CMD ["/bin/bash"] 0 B <missing> 5 months ago /bin/sh -c sed -i 's/^#\s*\(deb.*universe\)$/ 1.879 kB <missing> 5 months ago /bin/sh -c echo '#!/bin/sh' > /usr/sbin/polic 701 B <missing> 5 months ago /bin/sh -c #(nop) ADD file:3f4708cf445dc1b537 131.3 MB
Quando criamos um container, o docker cria a camada usando o comando executado após o nome do container. Como não especificamos nenhum comando, o docker utilizou o comando padrão da imagem do ubuntu (/bin/bash) que, em conjunto com as opções -it, executou o container em modo interativo. Dessa forma, independente do que executarmos no container, o comando relacionado a camada será o mesmo. Ou seja, podemos instalar um pacote, criar arquivos e definir configurações no container, porém, ao listarmos as camadas da imagem apenas o comando /bin/bash será exibido.
Para contornar essa situação, devemos informar o comando desejado diretamente na criação do container. Dessa forma, além de agilizar o processo, saberemos exatamente qual instrução criou a camada.
Vamos refazer o processo anterior da seguinte forma:
root@docker-host:~# docker run ubuntu:15.04 /bin/sh -c 'echo "Ubuntu Modificado!" > /tmp/commit.txt' root@docker-host:~# docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 782ffd0a38ea ubuntu:15.04 "/bin/sh -c 'echo \"Ub" 6 minutes ago Up 6 minutes stupefied_saha root@docker-host:~# docker commit 782ffd0a38ea cmotta2016/ubuntu-modificado:1.1 sha256:adf8061b924a66b57b9f90879a1b42e2627790a183cc13654c3c17502d4d600e root@docker-host:~# docker history cmotta2016/ubuntu-modificado:1.1 IMAGE CREATED CREATED BY SIZE COMMENT adf8061b924a 2 seconds ago /bin/sh -c echo "Ubuntu Modificado!" > /tmp/c 19 B d1b55fd07600 5 months ago /bin/sh -c #(nop) CMD ["/bin/bash"] 0 B <missing> 5 months ago /bin/sh -c sed -i 's/^#\s*\(deb.*universe\)$/ 1.879 kB <missing> 5 months ago /bin/sh -c echo '#!/bin/sh' > /usr/sbin/polic 701 B <missing> 5 months ago /bin/sh -c #(nop) ADD file:3f4708cf445dc1b537 131.3 MB root@docker-host:~# docker run --rm cmotta2016/ubuntu-modificado:1.1 cat /tmp/commit.txt Ubuntu Modificado!
Repare que agora podemos identificar exatamente qual instrução criou a camada.
Ao criar imagens a partir de containers, devemos ter em mente que o comando da última camada adicionada, define o novo comando padrão da imagem. Ou seja, o comando usado na criação do container que sofreu o commit será o novo comando padrão da imagem.
Vamos construir uma imagem do nginx para analisar esse conceito na prática. Para isso, crie um container da seguinte forma:
root@docker-host:~# docker run -d ubuntu:15.04 /bin/sh -c 'apt-get update && apt-get install nginx -y' ec7085576fc25754979ae39e155093689b15e3065120cd227eeac785c9fcb078
Criamos um container especificando o comando para instalar o nginx. Esse container foi criado em background (-d). Para exibir em tempo real as atividades no container, execute o comando docker logs –follow seguido do identificador (ou nome) do container.
Encerrado o processo de instalação, é hora de criar a imagem com o docker commit. Porém, como dissemos anteriormente, o comando executado na criação do container será o comando padrão da nova imagem. Nesse caso, ao efetuarmos o commit do container acima, o comando padrão da imagem será definido como /bin/sh -c ‘apt-get update && apt-get install nginx -y’. Isso significa que todo container, criado a partir dessa imagem, executará esse comando caso nenhum outro seja especificado. Vamos criar uma imagem a partir do container:
root@docker-host:~# docker commit ec7085576fc2 cmotta2016/nginx:1.0 sha256:d7ffc6e08c9c4a9f2f53b39dbd668fb9c74c5ae7c362e1acd0c1a7d8fbde32bd
Em seguida, crie um novo container sem especificar nenhum comando após o nome da imagem:
root@docker-host:~# docker run -it cmotta2016/nginx:1.0 Hit http://archive.ubuntu.com vivid InRelease Hit http://archive.ubuntu.com vivid-updates InRelease Hit http://archive.ubuntu.com vivid-security InRelease Get:1 http://archive.ubuntu.com vivid/main Sources [1358 kB] ...
Veja que o processo de atualização da lista de pacotes dos repositórios (apt-get update) inicia automaticamente no container. Pressione Ctrl+c para sair do container e encerrá-lo. Em seguida, examine qual é o comando padrão no container:
root@docker-host:~# docker inspect -f '{{json .Config.Cmd}}' 6263454f94c7 | python -m json.tool [ "/bin/sh", "-c", "apt-get update && apt-get install nginx -y" ]
Repare que esse é o mesmo comando executado no container que sofreu o commit – definido como padrão da imagem. A intenção é que os containers criados a partir dessa imagem iniciem automaticamente o nginx. Nesse caso, devemos definir um novo comando padrão para imagem modificada, e isso pode ser feito através da opção -c (–change) do comando docker commit. Com essa opção podemos aplicar instruções usadas pelo processo de criação automatizada (veremos na próxima seção), alterando algumas configurações da imagem.
Refaça o processo de criação da imagem da seguinte forma:
root@docker-host:~# docker commit --change='CMD ["nginx", "-g", "daemon off;"]' -c "EXPOSE 80" ec7085576fc2 cmotta2016/nginx:2.0
sha256:759a23bf9140a374bd05f4c181891521233b77858459060b1ec0109f3abd2f3a
A instrução CMD será abordada mais a frente. Por hora basta saber que essa instrução define o comando padrão da imagem. Dessa forma, todo container criado a partir dessa imagem, irá executar o comando nginx -g daemon off; ao ser iniciado. Repare que além de definirmos o comando padrão, também configuramos a porta usada pela aplicação (EXPOSE 80). Sendo assim, podemos criar um novo container a partir dessa imagem e testar a aplicação:
root@docker-host:~# docker run -d -P cmotta2016/nginx:2.0 090171709a4bee3898099eab3216f17d2f4bdd672ba5b0426d9667800b86fe29 root@docker-host:~# docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 090171709a4b cmotta2016/nginx:2.0 "nginx -g 'daemon off" 3 seconds ago Up 2 seconds 0.0.0.0:32769->80/tcp elegant_kowalevski
O comando docker commit nos permite ainda, adicionar um comentário na imagem. Esse comentário será associado a camada adicionada, e deve ser passado com a opção -m do comando.
Outra característica é que ao executar o commit, o docker pausa o container durante o processo para evitar que dados sejam corrompidos no processo de criação. Apesar de ser o comportamento padrão, podemos alterá-lo usando a opção –pause=false. Vejamos na prática.
Crie um container a partir da imagem do ubuntu:15.04. Em seguida saia do container mantendo-o em execução. Não é necessário efetuar nenhuma alteração dentro do container:
root@docker-host:~# docker run -it ubuntu:15.04 root@d7f05ed056b2:/# root@docker-host:~# docker ps -a -q d7f05ed056b2
Agora abra outro terminal no host docker e execute o comando docker events. Esse comando é usado para monitorar em tempo real os eventos do servidor.
Retorne ao primeiro terminal e crie uma imagem conforme abaixo:
root@docker-host:~# docker commit -m "Nenhuma alteração efetuada!" d7f05ed056b2 cmotta2016/ubuntu-modificado:2.0
sha256:8f1261573261713c2c2453e1689e5bea1a22f0446a6f1f05aa2c0dae47569614
Examine a saída do comando docker events e repare nos eventos containers pause e container unpause:
root@docker-host:~# docker events 2016-06-28T14:00:40.294491807-03:00 container pause d7f05ed056b2... (image=ubuntu:15.04, name=determined_hugle) 2016-06-28T14:00:40.314916508-03:00 image tag sha256:8f1261573261... (name=cmotta2016/ubuntu-modificado:2.0) 2016-06-28T14:00:40.314969193-03:00 container commit d7f05ed056b2... (comment=Nenhuma alteração efetuada!, image=ubuntu:15.04, name=determined_hugle) 2016-06-28T14:00:40.322649103-03:00 container unpause d7f05ed056b2... (image=ubuntu:15.04, name=determined_hugle)
Em seguida, examine a imagem criada e veja que o comentário foi adicionado na camada:
root@docker-host:~# docker history cmotta2016/ubuntu-modificado:2.0
IMAGE CREATED CREATED BY SIZE COMMENT
8f1261573261 1 minutes ago /bin/bash 0 B Nenhuma alteração efetuada!
...
Crie uma nova imagem, usando a opção –pause=false conforme abaixo. Em seguida analise os eventos:
root@docker-host:~# docker commit -m "Nenhuma alteração efetuada!" --pause=false d7f05ed056b2 cmotta2016/ubuntu-modificado:2.1 ... 2016-06-28T14:05:23.290417706-03:00 image tag sha256:c6380c573bd2... (name=cmotta2016/ubuntu-modificado:2.1) 2016-06-28T14:05:23.290478770-03:00 container commit d7f05ed056b2... (comment=Nenhuma alteração efetuada!, image=ubuntu:15.04, name=determined_hugle)
Repare que dessa vez nenhuma mensagem de pause foi informada.
Antes de prosseguirmos, vale ressaltar que o tamanho da camada equivale as alterações efetuadas pelo comando executado na criação do container. Isso significa que quanto mais modificações efetuarmos no container, maior será o tamanho final da imagem.
Modo Automatizado
O segundo modo de construção de imagens executa todos os processos de forma automática. Esse método é iniciado pelo comando docker build, que possui diversas opções disponíveis. A sintaxe desse comando é docker build [OPTIONS] PATH | URL | –, onde o path é um diretório local do host, enquanto a url indica a localização de um repositório GIT. Esses dois componentes são conhecidos como contexto – localização utilizada para armazenar os arquivos usados na construção da imagem. No caso do path normalmente cria-se um diretório exclusivo para armazenar os arquivos, o que facilita a organização no processo de criação das imagens.
Ao executarmos o comando docker build, o daemon busca as instruções necessárias para a criação da imagem em um arquivo chamado Dockerfile. Por padrão, o docker busca esse arquivo no diretório atual do usuário que executou o comando (PATH/Dockerfile). No entanto, é possível instruir o daemon a utilizar o arquivo em outra localização usando a opção -f.
O arquivo Dockerfile utiliza o formato INSTRUÇÃO argumento. Como vimos anteriormente, cada instrução executa uma ação (adicionar arquivos, executar comandos, definir configurações, etc.), e é responsável pela criação de uma camada da imagem. Examine a imagem do ubuntu:15.04 abaixo e repare nas instruções das camadas (ADD, RUN e CMD):
O docker build cria containers temporários para executar cada instrução do Dockerfile, empilhando as camadas desses containers sobre a imagem base. Após executar a instrução, o daemon faz o commit da camada do container (gerando uma imagem intermediária) e o remove. Em seguida, usa a imagem intermediária como base para executar a próxima instrução e assim por diante, até que a última instrução seja executada.
Para entendermos melhor a função e a utilização das instruções, assim como o comando docker build, vamos construir uma imagem do nginx de forma automatizada. Primeiro, vamos criar um diretório e um arquivo Dockerfile dentro dele:
root@docker-host:~# mkdir build_nginx && cd build_nginx
root@docker-host:~/build_nginx# vim Dockerfile
## Dockerfile para construção de uma imagem do nginx.
FROM ubuntu:15.04
# Define a imagem base para as instruções subsequentes.
MAINTAINER Carlos Motta <motta.carlos08@gmail.com>
# Definindo o autor da imagem.
RUN apt-get update && apt-get install nginx -y
# Define o comando para instalação do nginx.
CMD ["nginx", "-g", "daemon off;"]
# Define o comando padrão da imagem.
EXPOSE 80 443
# Define as portas que serão expostas nos containers.
A instrução FROM define a imagem que será usada como base para criação da nova imagem, ou seja, a imagem onde as demais configurações serão definidas. Essa instrução não cria um container. A instrução MAINTAINER por sua vez, define o nome do autor da imagem. A mesma informação pode ser definida com a opção -a do comando docker commit.
Os comandos que serão executados na criação da imagem são definidos na instrução RUN. Em nosso caso, definimos os comandos para atualizar a lista de pacotes dos repositórios e instalar o nginx.
Da forma como mencionado no arquivo, os comandos serão executadas em modo shell, invocados como um subcomando do sh (shell padrão do docker). Isso significa que o daemon, ao ler essa instrução, vai criar um container executando o comando /bin/sh -c ‘apt-get update && apt-get install nginx -y’.
Instruções RUN podem ser especificadas quantas vezes forem necessárias no Dockerfile. No arquivo que criamos, por exemplo, poderíamos defini-la uma vez para o comando de atualização (RUN apt-get update) e outra para o comando de instalação (RUN apt-get install nginx -y). Nesse caso, cada instrução adicionaria uma camada na imagem:
... c5a7de0c1a86 36 minutes ago /bin/sh -c apt-get install nginx -y 52.26 MB 16d97edd46a0 38 minutes ago /bin/sh -c apt-get update 21.86 MB ...
A próxima instrução no arquivo é a CMD. Ela é usada basicamente para definir o comando padrão da imagem, ou seja, permite configurar qual comando os containers executarão ao serem criados. Uma instrução semelhante (ENTRYPOINT) pode ser usada para o mesmo fim. A diferença está na forma como o comando definido pode ser sobrescrito. Vamos entender esse conceito.
A instrução CMD corresponde ao campo COMMAND da sintaxe do comando docker run (docker run [OPTIONS] IMAGE[:TAG] [COMMAND] [ARG…]). Caso nenhum comando seja passado na criação do container, o docker executa automaticamente o comando definido nessa instrução. De outra forma, o comando passado na criação do container sobrescreve o padrão definido pela instrução CMD. Por exemplo, o CMD da imagem do ubuntu:15.04 é /bin/bash. Abaixo criamos um container sem passar nenhum comando após o nome da imagem:
root@docker-host:~# docker run ubuntu:15.04 root@docker-host:~# docker ps -a CONTAINER ID IMAGE COMMAND 01e55caef713 ubuntu:15.04 "/bin/bash"
Veja que o comando padrão da imagem foi executado.
Em seguida criamos um container definindo um comando simples:
root@docker-host:~# docker run ubuntu:15.04 echo docker docker root@docker-host:~# docker ps -a CONTAINER ID IMAGE COMMAND bac9873909fb ubuntu:15.04 "echo docker"
Dessa vez o comando padrão (/bin/bash) foi sobrescrito pelo comando passado na criação do container (echo docker).
A instrução ENTRYPOINT por sua vez, é propositalmente mais difícil de sobrescrever. Quando usada, o docker não sobrescreve seu valor pelo comando passado na criação do container. Ao invés disso o comando será usado como argumento para o entrypoint. Veja abaixo o exemplo de uma imagem com o ENTRYPOINT definido como /bin/bash:
root@docker-host:~# docker run ubuntu-entrypoint root@docker-host:~# docker ps -a CONTAINER ID IMAGE COMMAND 4016ad4caca9 ubuntu-entrypoint "/bin/bash" root@docker-host:~# docker run ubuntu-entrypoint -c 'echo docker' docker root@docker-host:~# docker ps -a CONTAINER ID IMAGE COMMAND c4a461d03312 ubuntu-entrypoint "/bin/bash -c 'echo docker'"
Note que o comando passado após o nome da imagem não sobrescreveu o padrão definido pela instrução. Ao invés disso, ele foi usado como argumento para o entrypoint.
Ainda podemos alterar o padrão definido por essa instrução usando a opção –entrypoint do comando docker run:
root@docker-host:~# docker run --entrypoint=/bin/sh ubuntu-entrypoint root@docker-host:~# docker ps -a CONTAINER ID IMAGE COMMAND 9dbd968b3ce1 ubuntu-entrypoint "/bin/sh" root@docker-host:~# docker run --entrypoint=/bin/sh ubuntu-entrypoint -c 'echo docker' root@docker-host:~# docker ps -a CONTAINER ID IMAGE COMMAND 100b7c84fb40 ubuntu-entrypoint "/bin/sh -c 'echo docker'"
Além do uso individual das instruções CMD e ENTRYPOINT, também podemos definir ambas no Dockerfile. Nesse caso, o valor da instrução CMD será passado automaticamente como argumento para o comando da instrução ENTRYPOINT.
Utilizando o exemplo citado acima, poderíamos definir o Dockerfile da seguinte forma:
FROM ubuntu:15.04 ENTRYPOINT ["/bin/sh"] CMD ["-c", "echo docker"]
Ao criar o container sem passar nenhum comando, observamos o comando padrão sendo executado. Caso contrário, o comando especificado na criação do container irá sobrescrever o CMD:
root@docker-host:~# docker run ubuntu-entrypoint-cmd docker root@docker-host:~# docker ps -a CONTAINER ID IMAGE COMMAND 3e8701c78506 ubuntu-entrypoint-cmd "/bin/sh -c 'echo docker'" root@docker-host:~# docker run ubuntu-entrypoint-cmd -c 'cat /etc/hostname' 09f288b02ab9 root@docker-host:~# docker ps -a CONTAINER ID IMAGE COMMAND 09f288b02ab9 ubuntu-entrypoint-cmd "/bin/sh -c 'cat /etc"
Essas duas instruções podem ser escritas na forma executável ou no formato shell. Em nosso arquivo Dockerfile a instrução CMD foi escrita na forma executável. Isso significa que containers criados a partir dessa imagem executarão o comando definido sem invocar nenhum shell (nginx -g ‘daemon off;’). Ao usar o formato shell (CMD nginx -g “daemon off;”), a instrução será executada como um subcomando do shell padrão do docker, o /bin/sh (/bin/sh -c ‘nginx -g ‘daemon off;’), e isso pode ser um problema.
O comando executado na criação do container, seja especificado manualmente ou definido por uma instrução, dá origem ao primeiro processo dentro do container (PID 1). Ao encerrarmos um container com o comando docker stop, o daemon envia o sinal padrão SIGTERM (15) para o processo de PID 1, e aguarda até que o container seja encerrado. Caso o container não seja encerrado dentro de um espaço de tempo, o daemon envia um sinal SIGKILL (9) para esse processo, “forçando” o encerramento do container.
Instruções CMD e ENTRYPOINT definidas no formato shell, como dissemos, serão executadas como um subcomando do shell sh. Nesse caso, o primeiro processo (PID 1) do container será o /bin/sh, que não repassa sinal SIGTERM. Sendo assim, o daemon vai encerrar o container de maneira irregular. A diferença pode ser percebida tanto pelo tempo de encerramento dos containers, quanto pela observação dos eventos.
No exemplo a seguir, criamos um container sobre uma imagem cuja instrução CMD foi definida no modo executável:
## Dockerfile CMD ["nginx", "-g", "daemon off;"] root@docker-host:~# docker inspect -f '{{json .Config.Cmd}}' 4109187f8c3c | python -m json.tool [ "nginx", "-g", "daemon off;" ] root@docker-host:~# docker exec -it 4109187f8c3c ps aux USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND root 1 0.0 0.8 121156 8920 ? Ss 14:39 0:00 nginx: master process nginx -g daemon off; root@docker-host:~# time docker stop 4109187f8c3c 4109187f8c3c real 0m0.547s user 0m0.008s sys 0m0.012s root@docker-host:~# docker events 2016-06-30T11:35:28.843673515-03:00 container kill 4109187f8c3c... signal=15) 2016-06-30T11:35:28.905682025-03:00 container die 4109187f8c3c... ...
Repare que o primeiro processo do sistema foi criado a partir do comando definido no formato executável da imagem. Desse modo o daemon conseguiu encerrar o container com o comando padrão (15), levando pouco mais de meio segundo para isso.
Agora veja o exemplo de uma imagem com a instrução CMD definida em formato shell:
## Dockerfile CMD nginx -g "daemon off;" root@docker-host:~# docker inspect -f '{{json .Config.Cmd}}' 64e6727f216d | python -m json.tool [ "/bin/sh", "-c", "nginx -g \"daemon off;\"" ] root@docker-host:~# docker exec -it 64e6727f216d ps aux USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND root 1 0.1 0.0 4472 752 ? Ss 12:19 0:00 /bin/sh -c nginx -g "daemon off;" root 5 0.1 0.8 121156 8764 ? S 12:19 0:00 nginx: master process nginx -g daemon off; ... root@docker-host:~# time docker stop 64e6727f216d 64e6727f216d real 0m10.520s user 0m0.012s sys 0m0.004s root@docker-host:~# docker events 2016-06-30T11:35:54.766315874-03:00 container kill 64e6727f216d... signal=15) 2016-06-30T11:36:04.767836118-03:00 container kill 64e6727f216d... signal=9) 2016-06-30T11:36:04.818420121-03:00 container die 64e6727f216d...
Como dissemos, o primeiro processo do sistema foi iniciado pelo shell (sh). Então, ao executar o docker stop, o docker enviou o comando padrão (15), porém não conseguiu encerrar o container. Então, cerca de 10 segundos depois, o daemon enviou o sinal SIGKILL (9), forçando o encerramento do container.
Caso seja necessário definir a instrução no formato shell, podemos utilizar o exec antes do comando na instrução. Esse argumento instrui o docker a tratar tal instrução no formato executável.
Refazendo o último exemplo dessa forma, podemos notar que o container será encerrado da forma correta:
## Dockerfile
CMD exec nginx -g "daemon off;"
root@docker-host:~/build_nginx# docker inspect -f '{{json .Config.Cmd}}' 0a1e6aa52539 | python -m json.tool
[
"/bin/sh",
"-c",
"exec nginx -g \"daemon off;\""
]
root@docker-host:~/build_nginx# docker exec -it 0a1e6aa52539 ps aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.8 121156 8936 ? Ss 12:42 0:00 nginx: master p
root@docker-host:~/build_nginx# time docker stop 0a1e6aa52539
0a1e6aa52539
real 0m0.609s
user 0m0.016s
sys 0m0.004s
root@docker-host:~# docker events
2016-07-08T09:44:30.133155825-03:00 container kill 0a1e6aa52539... signal=15)
2016-07-08T09:44:30.197659548-03:00 container die 0a1e6aa52539...
Antes de prosseguirmos para próxima instrução do nosso arquivo Dockerfile, vale ressaltar que a instrução CMD, assim como a ENTRYPOINT, deve ser definida apenas uma vez no arquivo Dockerfile. Caso contrário, apenas a última instrução definida terá efeito sobre os containers criados a partir da imagem. De modo geral, isso significa que um container poderá “rodar” apenas um executável quando iniciado. Para permitir a execução de múltiplos processos simultaneamente, podemos escrever um script ou utilizar uma ferramenta de gerenciamento de processos. Por exemplo, podemos criar uma imagem para executar o nginx e o ssh usando o supervisor (gerenciador de processos). Nesse caso, os containers criados sobre essa imagem, invocam o supervisor como executável padrão, que por sua vez inicia os demais executáveis configurados.
A última instrução do nosso Dockerfile (EXPOSE) define quais portas dos containers serão exportadas. Vale ressaltar que essa instrução não faz o mapeamento das portas, apenas define quais portas poderão ser mapeadas automaticamente através da opção -P. Além disso, mesmo que essa instrução não seja definida no arquivo, ainda podemos mapear portas manualmente usando a opção -p do comando docker run.
Feitas as considerações, vamos construir a imagem. Execute o comando abaixo:
root@docker-host:~/build_nginx# docker build -t cmotta2016/nginx:3.0 . Sending build context to Docker daemon 5.12 kB Step 1 : FROM ubuntu:15.04 15.04: Pulling from library/ubuntu ... Digest: sha256:2fb27e433b3ecccea2a14e794875b086711f5d49953ef173d8a03e8707f1510f Status: Downloaded newer image for ubuntu:15.04 ---> d1b55fd07600 Step 2 : MAINTAINER Carlos Motta <motta.carlos08@gmail.com> ---> Running in dc951839b187 ---> 4b021289e7e3 Removing intermediate container dc951839b187 Step 3 : RUN apt-get update && apt-get install nginx -y ---> Running in 88c4fd7d5f2c Hit http://archive.ubuntu.com vivid InRelease Get:1 http://archive.ubuntu.com vivid-updates InRelease [65.9 kB] Get:2 http://archive.ubuntu.com vivid-security InRelease [65.9 kB] ... Processing triggers for sgml-base (1.26+nmu4ubuntu1) ... Processing triggers for systemd (219-7ubuntu6) ... ---> 38e4f0b213d6 Removing intermediate container 88c4fd7d5f2c Step 4 : CMD nginx -g daemon off; ---> Running in 190e02e31242 ---> fc0d3245b8df Removing intermediate container 190e02e31242 Step 5 : EXPOSE 80 443 ---> Running in d89fc6b5dfd8 ---> 42cf0d76a256 Removing intermediate container d89fc6b5dfd8 Successfully built 42cf0d76a256
Usamos apenas a opção -t para definir o nome da imagem. Estamos executando o comando no mesmo diretório do Dockerfile, logo, não precisamos especificar a localização do arquivo.
Como dissemos anteriormente, o daemon cria automaticamente os containers para executar as instruções e em seguida os remove. Repare que cada instrução começa com a mensagem Running in… (seguida do id do container), e termina com Removing intermediate container. Esse comportamento pode ser alterado usando a opção –rm=false.
Outro ponto que vale ressaltar, é o uso de cache no processo de criação automatizada. Ao criarmos uma imagem com o docker build, o docker mantém em cache a imagem base usada no processo de criação. Na próxima criação de uma imagem, o docker faz uma busca por imagens em cache que possa reutilizar. Se a imagem base for encontrada no cache, o docker compara a instrução a ser executada com todas as imagens intermediárias, criadas a partir da imagem base, verificando se alguma delas foi criada usando exatamente a mesma instrução. Caso positivo o docker reutiliza essa imagem apresentando a informação using cache na saída do docker build.
Podemos testar esse conceito recriando a imagem acima. Edite o arquivo Dockerfile e altere apenas a instrução EXPOSE removendo a porta 443. Em seguida crie a nova imagem e veja o resultado:
root@docker-host:~/build_nginx# docker build -t cmotta2016/nginx:3.1 . Sending build context to Docker daemon 5.12 kB Step 1 : FROM ubuntu:15.04 ---> d1b55fd07600 Step 2 : MAINTAINER Carlos Motta <motta.carlos08@gmail.com> ---> Using cache ---> 4b021289e7e3 Step 3 : RUN apt-get update && apt-get install nginx -y ---> Using cache ---> 38e4f0b213d6 Step 4 : CMD nginx -g daemon off; ---> Using cache ---> fc0d3245b8df Step 5 : EXPOSE 80 ---> Running in ff68a0301845 ---> 4f50670f6a64 Removing intermediate container ff68a0301845 Successfully built 4f50670f6a64
Nesse caso, criamos uma nova imagem (cmotta2016/nginx:3.1) alterando apenas as portas. O docker utilizou o cache em todas as instruções que não sofreram alteração (using cache). Como alteramos a instrução EXPOSE o daemon não utilizou o cache, executando a instrução normalmente.
OBS: O uso de cache no processo de building faz a comparação apenas da string do comando executado, e não dos arquivos no container. Isso significia que ao executar a instrução apt-get update por exemplo, o daemon não faz a checagem dos pacotes atualizados em si, apenas do comando. O docker só examina o conteúdo de arquivos quando as instruções são COPY e ADD – instruções usadas para copiar arquivos para a imagem.
Por fim, execute um comando a partir da imagem criada e teste o acesso ao nginx:
root@docker-host:~/build_nginx# docker run -d -P --name my-nginx cmotta2016/nginx:3.0 d3a645853c906e15a6d28eea933e815cbf733d80ccfc7f111f49b9a11ab01c38 root@docker-host:~/build_nginx# docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES d3a645853c90 cmotta2016/nginx:3.0 "nginx -g 'daemon off" 3 seconds ago Up 1 seconds 0.0.0.0:32769->80/tcp, 0.0.0.0:32768->443/tcp my-nginx
Existem outras opções de instruções disponíveis para uso no Dockerfile como ADD e COPY (mencionadas acima), e ENV, usada para definir variáveis de ambiente. A lista completa das instruções, além da utilização básica de cada uma, podem ser obtidas com o comando man dockerfile.
Bom pessoal, chegamos ao final de mais um post. Caso você tenha criado suas próprias imagens, não esqueça de enviar para o registro com o comando docker push <imagem>. No próximo post abordaremos uma estrutura docker em cluster. Até a próxima e não esqueça de deixar seu comentário.
Muito bom tutorial. Mas ajudou a entender melhor o conceito. Tive que aprender na marra devido a necessidade da nova empresa que estou. Obrigado!
E eu também estou tentando aprender na marra!