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 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