Oi, pessoal!
Tivemos diversas novidades com o lançamento das versões 17.05 e 17.06 do Docker, que ocorreram nos últimos meses. Nosso objetivo é trazer para vocês algumas dessas novidades. Iniciaremos com o multi-stage builds, ou em português claro: construção em múltiplos estágios. Vamos entender um pouco mais sobre esse conceito, como utilizá-lo e onde ele pode te ajudar no dia-a-dia.
O que é?
O multi-stage build foi lançado na versão 17.05 e permite que um build possa ser reutilizado em diversas etapas da geração da imagem, deixando os Dockerfiles mais fáceis de ler e manter.
O estado da arte
Uma das coisas mais desafiadoras sobre a construção de imagens é manter o tamanho da imagem reduzido. Cada instrução no Dockerfile adiciona uma camada à imagem, e você precisa se lembrar de limpar todos os artefatos que não precisa antes de passar para a próxima camada. Para escrever um Dockerfile realmente eficiente, você tradicionalmente precisa empregar truques de shell e outra lógica para manter as camadas o mais pequenas possíveis e garantir que cada camada tenha os artefatos que ela precisa da camada anterior e nada mais.
Na verdade, era muito comum ter um Dockerfile para uso para o desenvolvimento (que continha tudo o que era necessário para construir sua aplicação), e outro para usar em produção, que só continha sua aplicação e exatamente o que era necessário para executá-la. Obviamente, a manutenção de dois Dockerfiles não é o ideal.
Aqui está um exemplo de Dockerfile.build e Dockerfile exemplifica o caso acima:
Dockerfile.build:
FROM golang:1.7.3 WORKDIR /go/src/github.com/alexellis/href-counter/ RUN go get -d -v golang.org/x/net/html COPY app.go . RUN go get -d -v golang.org/x/net/html \ && CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .
Observe que este exemplo também comprime artificialmente dois comandos RUN, juntando-os com o parâmetro “&&” do bash para evitar criar uma camada adicional na imagem. Isso é propenso a falhas e difícil de manter. É fácil inserir outro comando e esquecer de continuar a linha usando este parâmetro, por exemplo.
Dockerfile:
FROM alpine:latest RUN apk --no-cache add ca-certificates WORKDIR /root/ COPY app . CMD ["./app"]
Build.sh:
#!/bin/sh echo "Building alexellis2/href-counter:build" docker build --build-arg https_proxy=$https_proxy --build-arg http_proxy=$http_proxy \ -t alexellis2/href-counter:build . -f Dockerfile.build docker create --name extract alexellis2/href-counter:build docker cp extract:/go/src/github.com/alexellis/href-counter/app ./app docker rm -f extract echo "Building alexellis2/href-counter:latest" docker build --no-cache -t alexellis2/href-counter:latest . rm ./app
Quando você executa o script build.sh, ele cria a primeira imagem com o artefato, a partir da qual cria-se um container que é utilizado para copiar o artefato. Em seguida, ele cria a segunda imagem copiando o artefato para essa segunda imagem. Ambas as imagens ocupam espaço em seu sistema e você ainda tem o artefato em seu disco local também.
O que melhorou
Com o multi-stage, você usa várias instruções FROM no seu Dockerfile, cada instrução FROM pode usar uma base diferente, e cada uma delas começa um novo estágio da compilação. Você pode copiar artefatos de um estágio para outro, deixando para trás tudo que você não quer na imagem final. Para mostrar como isso funciona, vamos adaptar o Dockerfile anterior para usar multi-stage:
Dockerfile:
FROM golang:1.7.3 WORKDIR /go/src/github.com/alexellis/href-counter/ RUN go get -d -v golang.org/x/net/html COPY app.go . RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app . FROM alpine:latest RUN apk --no-cache add ca-certificates WORKDIR /root/ COPY --from=0 /go/src/github.com/alexellis/href-counter/app . CMD ["./app"]
Você só precisa do único Dockerfile, além de não precisar de um script de compilação separado. Basta buildar sua imagem docker:
$ docker build -t alexellis2/href-counter:latest .
O resultado final é a mesma pequena imagem de produção que antes, com uma redução significativa na complexidade. Você não precisa criar nenhuma imagem intermediária e você não precisa extrair nenhum artefato para o seu sistema local.
Como funciona? A segunda instrução FROM inicia um novo estágio de compilação com a imagem base sendo alpine. A instrução “COPY –from=0” copia apenas o artefato construído do estágio anterior para esta nova imagem, o Go SDK e quaisquer artefatos intermediários são deixados para trás e não são salvos na imagem final.
Deixando mais claro
Por padrão, as etapas não são nomeadas, e você referencia elas por seu número inteiro, começando por 0 na primeira instrução FROM. No entanto, você pode nomear seus estágios, adicionando a instrução “as <nome>” na mesma linha do FROM. O exemplo abaixo deixa mais claro isso e melhora a forma como manipulamos nossos builds nomeando as etapas e usando o nome na instrução COPY. Isso significa que, mesmo que as instruções no seu Dockerfile sejam reordenadas, a cópia do artefato não será interrompida.
FROM golang:1.7.3 as builder WORKDIR /go/src/github.com/alexellis/href-counter/ RUN go get -d -v golang.org/x/net/html COPY app.go . RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app . FROM alpine:latest RUN apk --no-cache add ca-certificates WORKDIR /root/ COPY --from=builder /go/src/github.com/alexellis/href-counter/app . CMD ["./app"]
Ficou fácil, né? Essa feature auxilia ainda mais as equipes no momento de administrar seus builds, pois centraliza e deixa mais transparente cada passo na geração dos pacotes/artefatos de uma aplicação.
Gostaríamos de agradecer ao @alexellisuk pela contribuição a comunidade com os exemplos utilizados acima. Esperamos ter ajudado e, como sempre, se tiver dúvidas avisa aí que vamos te ajudar.
Grande abraço!