Lazaro Fernandes Lima Suleiman

Lazaro Fernandes Lima Suleiman

head of technology at @Hondana

Otimize o build das suas imagens docker com multi-stage

Neste post vamos falar um pouco sobre como criar imagens docker otimizadas para nossas aplicações através do multi-stage build.

A partir da versão 17.05 do Docker, lançada a 1 ano atrás, o time de desenvolvimento disponibilizou suporte a multi-stage build, basicamente, são extensões dos comandos FROM e COPY que nos permite criar um Dockerfile com múltiplas fases de build, ou steps.

Para este post irei utilizar como exemplo uma aplicação ASP.NET Core 2, onde o processo de build da imagem irá conter múltiplas fases, como restore, testes e etc. Nossa imagem final irá conter apenas o ambiente mínimo necessário para rodar a aplicação, sem dependências não produtivas.

Você pode baixar todos os arquivos do projeto no github e pular para a parte do Dockerfile, mas se desejar, vamos criar juntos o projeto web e os testes.

Aplicação

Iremos utilizar uma aplicação web convencional, criando através do cli.
Estou utilizando Ubuntu 16 para criar este post, mas você pode utilizar windows sem problemas.

No seu terminal, crie uma aplicação web e certifique-se de que ela esteja abrindo no navegador.

1
2
3
4
5
6
7
8
9
10
11
mkdir aspnetproject && cd aspnetproject
mkdir web
dotnet new sln
dotnet new web -o web
dotnet sln add web/web.csproj && dotnet restore
cd web && dotnet run

Hosting environment: Production
Content root path: /home/lazaro/aspnetproject/web
Now listening on: http://localhost:5000
Application started. Press Ctrl+C to shut down.

Com isso já temos nossa aplicação web disponível em http://localhost:5000/.

O MRDL

Testes

Iremos criar um projeto de teste para que possamos usar como exemplo.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
cd .. && mkdir tests && dotnet new mstest -o tests
dotnet sln add tests/tests.csproj
dotnet test tests/tests.csproj

Build started, please wait...
Build completed.

Test run for /home/lazaro/aspnetproject/tests/bin/Debug/netcoreapp2.0/tests.dll(.NETCoreApp,Version=v2.0)
Microsoft (R) Test Execution Command Line Tool Version 15.3.0-preview-20170628-02
Copyright (c) Microsoft Corporation. All rights reserved.

Starting test execution, please wait...

Total tests: 1. Passed: 1. Failed: 0. Skipped: 0.
Test Run Successful.
Test execution time: 1.3408 Seconds

Agora que temos nossa aplicação e nossos testes, vamos colocar tudo isso dentro de uma imagem.

Dockerfile e as mudanças no FROM e no COPY

Antes de criamos nosso Dockerfile vamos entender quais melhorias ocorreram no FROM e no COPY.

Anteriormente, tinhamos apenas um comando FROM que era carregado inicialmente e estabelecia qual imagem seria utilizada, afetando todos os comandos subsequentes.

Com multi-stages podemos utilizar quantos comandos FROM desejarmos. Cada FROM é um novo estágio que substitui o anterior, é de fato uma nova imagem, totalmente independente e isolada. Desta forma, se no estágio inicial estivermos utilizando uma imagem com .NET Core e no último estágio estamos utilizando uma imagem sem suporte a .net core a imagem final gerada ficará sem este suporte.

Outra mudança no FROM ocorreu em sua assinatura, agora existe a possibilidade de nomear os estágios através da instrução as.

1
2
3
4
5
6
7
8
FROM microsoft/aspnetcore-build:1.1 as build
...
...

FROM microsoft/dotnet:1.1-runtime as release
...
...
...

Neste primeiro momento podemos notar que cada etapa do build pode conter a imagem mais eficiente possível, seja uma etapa para executar um teste ou rodar a aplicação final.

Porém, nada disso faz muito sentido sem a possibilidade de utilizarmos informações das etapas anteriores, por isso, o comando COPY possui o argumento --from=<nome do estágio>, permitindo assim a copia de arquivos existentes em etapas anteriores

Exemplo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
FROM microsoft/aspnetcore-build:1.1 as build
WORKDIR /publish
COPY .bowerrc bower.json ./
RUN bower install
COPY my-app.csproj .
RUN dotnet restore
COPY . .
RUN dotnet publish --output ./out


FROM microsoft/dotnet:1.1-runtime as release
WORKDIR /dotnetapp
COPY --from=build /publish/out .
ENV ASPNETCORE_URLS "http://0.0.0.0:5000/"
ENTRYPOINT ["dotnet", "my-app.dll"]

Criando nosso Dockerfile

Nossa aplicação possui uma pasta para o projeto web e outra para o projeto de testes, precisamos garantir que somente se os testes estiverem passando a imagem será gerada.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# Restaura e copia os arquivos do projeto
FROM microsoft/aspnetcore-build:2.0 AS base-env
COPY . .
RUN dotnet restore

# Roda os testes de unidade
FROM microsoft/aspnetcore-build:2.0 AS test-env
WORKDIR /tests
COPY --from=base-env ./tests .
RUN dotnet test tests.csproj

# Publica o projeto web
FROM microsoft/aspnetcore-build:2.0 AS build-env
WORKDIR /app
COPY --from=base-env ./web .
RUN dotnet publish web.csproj -c Release -o out

# Copia a pasta do projeto web publicado e roda a aplicação web
FROM microsoft/aspnetcore:2.0
WORKDIR /app
COPY --from=build-env /app/out .
ENTRYPOINT ["dotnet", "web.dll"]

Nas primeiras 3 etapas estamos utilizando a imagem oficial para o processo de build de aplicações ASP.NET Core 2.

FROM microsoft/aspnetcore-build:2.0

Esta imagem já possui alguns recursos como:

Na primeira etapa eu copio os arquivos do projeto e faço o restore.

1
2
COPY . .
RUN dotnet restore

Na segunda etapa, o qual chamei de test-env, estou setando uma pasta local como pasta padrão /tests, copiando apenas a pasta com o projeto de testes a partir do base-env e rodando os testes.

1
2
3
WORKDIR /tests
COPY --from=base-env ./tests .
RUN dotnet test tests.csproj

Note que, nesse momento o contexto da imagem anterior já não é utilizado, logo, a pasta tests precisa ser copiada para a minha nova etapa através do comando COPY com o argumento --from=base.

Crio então uma nova etapa para a realização do publish do projeto web.

1
2
3
4
FROM microsoft/aspnetcore-build:2.0 AS build-env
WORKDIR /app
COPY --from=base-env ./web .
RUN dotnet publish web.csproj -c Release -o out

Na etapa final estou utilizando a imagem microsoft/aspnetcore:2.0, o qual é mais otimizada para executar a aplicação, não possuindo as dependências que são geralmente utilizadas para os processos de build e testes.

FROM microsoft/aspnetcore:2.0

Novamente, posso copiar a pasta com o projeto web que foi gerada em etapas anteriores e então definir o entrypoint para rodar a aplicação, mesmo que ela tenha utilizado uma imagem completamente diferente.

1
2
3
WORKDIR /app
COPY --from=build-env /app/out .
ENTRYPOINT ["dotnet", "web.dll"]

Espero que tenham gostado e que aproveitem ao máximo o multi-stage para criar imagens finais mais eficientes, seja em .NET ou quaisquer outras plataformas.

Comments