Neste post vou falar um pouco sobre como criar imagens docker otimizadas para nossas aplicações através do multi-stage build
.
Em muitos projetos notamos a presença de arquivos como Dockerfile.build
, Dockerfile.tests
e etc., arquivos estes que, representam etapas em processos de build e que são difíceis de criar e manter pelo time.
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, publicação e etc. Nossa imagem final irá conter apenas o ambiente mínimo necessário para rodar a aplicação e o pacote do projeto, sem dependências não produtivas, o que tornará a imagem final mais leve.
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.
Projeto Web
Iremos utilizar uma aplicação web convencional, criando através do cli.
Estou utilizando Ubuntu 16 para criar este post, mas você pode utilizar o SO de sua preferência.
Estou assumindo que você já possua o dotnet sdk e o docker instalados 😉
No seu terminal, crie uma aplicação web e certifique-se de que ela esteja abrindo no navegador.
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/.
Projeto de Testes
Iremos criar um projeto de teste para que possamos usar como exemplo.
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, é como 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
.
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:
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.
Com esse cenário em mente, se executarmos o teste dentro do build e ele retornar falha o processo é interrompido automaticamente.
# 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:
- .NET Core SDK
- NuGet com cache para pacotes comumente utilizados em projetos asp.net core
- Node.js
- Bower
- Gulp
Na primeira etapa eu copio os arquivos do projeto e faço o restore.
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.
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.
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.
WORKDIR /app COPY --from=build-env /app/out . ENTRYPOINT ["dotnet", "web.dll"]
Gere a imagem e em seguida rode em um novo container liberando a porta 8080.
docker build -t aspnetapp . docker run -p 8080:80 –name myapp aspnetapp
Tamanho final da imagem
Para efeito de comparação, caso utlizasse apenas um passo, com a imagem com todas as dependências, nossa imagem final teria em torno de 2GB, já com o uso de multi-stage podemos utilizar a imagem final de runtime, o qual possui 325MB + 3MB da nossa aplicação.
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.
(Crosspost de http://lazarofl.github.io/2018/04/17/Docker-MultiStage/)
Lazaro Fernandes Lima Suleiman
Desenvolvedor