Hoje veremos como definir os comandos do arquivo Dockerfile quando temos um projeto referenciando outro projeto. |
Se você não conhece o Docker sugiro que acompanhe a série de artigos : Docker uma introdução básica
(Bônus : Kubernetes essencial) |
A maioria dos exemplos mostra como dockerizar um projeto dotnet, supondo que ele não possui dependências locais. Então vamos analisar o que podemos fazer, quando nosso projeto tem referências a outros projetos na solução. Começaremos mergulhando em um exemplo simples sem dependências primeiro, para entender quais mudanças introduzimos e por quê.
Vamos partir do exemplo oficial de como dockerizar um projet .NET Core que mostra o seguinte código no arquivo Dockerfile localizado na pasta do projeto (onde o arquivo .csproj está armazenado):
E os comandos usados para criar a imagem a partir deste arquivo Dockerfile:
|
Vamos iniciar analizando as instruções do arquivo Dockerfile :
Instrução FROM
Nosso Dockerfile
começa com a instrução FROM:
FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build-env
Isso significa que baseamos nossa imagem no SDK oficial do Microsoft na versão 6.0. Usamos o SDK neste momento, não o runtime de produção, pois compilaremos nosso aplicativo no Docker durante a criação da imagem.
Portanto, você nem precisa ter o SDK instalado em sua máquina host, este Dockerfile é preparado de forma que você não compile seu aplicativo para a imagem do Docker - o Docker o compilará.
Você pode usar binários construídos em sua máquina host, mas não é seguro - pode não funcionar devido a problemas de compatibilidade.
Por que há instrução AS build-env ?
Veremos isso mais adiante...
Instrução WORKDIR
Na segunda linha, vemos a instrução WORKDIR /app,
o que significa que as instruções RUN, CMD, ENTRYPOINT,
COPY e ADD em nosso Dockerfile serão executadas no diretório
/app.
Se ele não existir, será criado (mesmo que não seja usado).
Instrução COPY
Em seguida, vemos a instrução COPY *.csproj ./ , o que significa que todos os arquivos csproj do contexto de compilação do Docker serão copiados para o diretório workdir (/app) dentro da imagem do Docker.
O comando de compilação do Docker será explicado posteriormente, mas em resumo - o contexto de compilação é o diretório da sua máquina host, apontado no comando de compilação do Docker. Se você apontar. path, o diretório onde você executa o comando é tomado. Portanto, no nosso caso, copiamos apenas um arquivo csproj, porque executamos o comando build com o diretório do projeto definido como contexto de compilação.
Instrução RUN
A seguir temos a instrução RUN dotnet restore, que simplesmente executa o comando dotnet restore em nosso diretório workdir (/app).
Neste momento dentro do diretório /app em nossa imagem, nada mais é que o .csproj do nosso projeto, pois copiamos apenas ele no passo anterior, mas é suficiente para restaurar as dependências do nuget.
Copiar e compilar a fonte do aplicativo
Novamente vemos a instrução COPY - COPY. ./
para copiar tudo do nosso contexto de compilação - no nosso caso, significa
arquivos de projeto (arquivos .cs etc.), porque executamos o comando
docker build com o diretório do projeto definido
como contexto de compilação.
Em seguida, com a instrução run - RUN dotnet publish -c Release -o out, simplesmente executamos dotnet publish em nosso diretório workdir (/app) dentro da imagem, com os parâmetros -c Release -o out.
Este comando dotnet compila nosso aplicativo com a configuração de release e publica os resultados no diretório out (no nosso caso /app/out). Podemos compilar o código-fonte porque baseamos essa imagem no SDK do desenvolvedor.
Compilação de vários estágios do Docker
Mais uma vez vemos a instrução FROM, que
define em qual imagem baseamos nossa imagem...
Como é possível especificá-la novamente, com uma base diferente?
É um recurso do Docker (desde a versão do Docker 17.05) chamado de compilações de vários estágios. Quando usamos a palavra-chave FROM novamente, queremos dizer que a imagem anterior especificada acima é temporária e foi usada apenas para servir a algum propósito.
No nosso caso foi feito apenas para compilar nossa aplicação - por isso usamos o SDK como imagem base. Agora especificamos a imagem base novamente e desta vez estamos preparando nossa imagem real - aquela que será implantada em produção, e esta não se baseia em SDK, apenas no runtime de produção, o que resulta em tamanho menor.
FROM mcr.microsoft.com/dotnet/aspnet:6.0
A seguir vamos apenas copiar nosso aplicativo compilado da imagem temporária.
Então, novamente, especificamos o diretório de trabalho para /app catalog e, em seguida, copiamos nossos binários - COPY --from=build-env /app/out . o que significa copiar arquivos de /app/out/ da imagem build-env (é por isso que demos um nome na primeira linha) para o diretório de trabalho atual (/app).
Instrução ENTRYPOINT
A última instrução neste Dockerfile é ENTRYPOINT, que (em termos simples) especifica um comando que será executado quando o contêiner for iniciado.
Então, no nosso caso - ENTRYPOINT ["dotnet", "aspnetapp.dll"] - o Docker executará o dotnet com o parâmetro aspnetapp.dll para iniciar nosso aplicativo.
Comando de compilação do Docker: build
Com base nesse Dockerfile, somos instruídos a executar o comando
docker build -t aspnetapp . no diretório do
projeto (onde o Dockerfile está armazenado).
A opção: -t name (--tag name) não é obrigatória - ela permite marcar a imagem (para nomeá-la e, opcionalmente e dar-lhe uma tag no formato 'name:tag'), então o comando principal é : docker build .
O contexto de compilação é o caminho na máquina host que estará acessível durante a criação da imagem para instruções do Dockerfile. No nosso caso é o caminho . o que significa que o diretório onde executamos este comando é passado como contexto de construção. Como nos dizem para executar este comando no diretório do projeto (onde o arquivo .csproj está armazenado), nossos arquivos de projeto são passados como contexto de compilação.
Comando de execução do Docker : run
O comando run do Docker cria um contêiner a partir da imagem. A imagem é um
manual somente leitura para o Docker, para criar o contêiner, e o contêiner está
funcionando na máquina virtual onde nosso aplicativo reside.
Podemos pensar assim: a imagem é como uma classe na programação orientada a objetos, e container é como uma instância, criada a partir dessa classe.
Assim, podemos criar quantos contêineres (instâncias) quisermos, e isso não afeta a imagem (classe) - a imagem é necessária apenas para que o Docker saiba como criar o contêiner.
Exemplo de comando
para criar o container:
docker run -d -p 8080:80
--name meucontainer aspnetapp
Sem a opção --detach (-d), começaremos a ver a saída do console do aplicativo do contêiner.
Com a opção --publish (-p) vinculamos a(s) porta(s) do contêiner ao host (por padrão com TCP, mas você também pode especificar UDP e SCTP).
Com a opção
--name atribuímos um nome ao container (sem esta
opção o Docker irá escolher algum nome engraçado para nós).
No final passamos o nome da imagem, que o Docker lerá para criar o container.
Como nomeamos nossa imagem como aspnetapp,
usamos esse nome aqui.
Docker com múltiplos projetos
Uma vez que
entendemos o que acontece no exemplo básico, vamos ver como alterá-lo, para
fazê-lo funcionar quando nosso projeto tem referências a outros projetos de
solução.
O problema é que executamos o comando build
do Docker a partir do diretório do projeto passando o caminho
. como contexto de construção. Isso significa que somente
os arquivos deste diretório estarão acessíveis durante a construção da imagem e,
dependendo dos projetos, é claro que os arquivos podem estar em outros
diretórios.
Como resolver isso ?
Temos várias opções para corrigir isso.
Podemos mover o Dockerfile um nível acima (para o diretório da solução) e executar o docker build a partir daí. Mas é recomendado ter o Dockerfile no diretório do projeto, para poder ter mais de um Dockerfile na solução (para diferentes projetos).
Você também pode executar o docker build como antes (do diretório do projeto), mas alterar o caminho do contexto de compilação para um nível acima (..).
Uma solução
elegante é executar o docker build do diretório da solução, passando
. como
contexto de compilação e para especificar qual Dockerfile queremos ler com a
opção --file (-f), da seguinte forma:
docker build -f PROJECT_DIRECTORY/Dockerfile -t
IMAGE_NAME .
Como ajustar o Dockerfile
Para que isso funcione precisamos ajustar o Dockerfile, porque o exemplo do Dockerfile padrão na página do Docker assume que temos o diretório do projeto como contexto de compilação.
Uma opção a isso seria usar este código:
Neste exemplo foi ignorado a restauração de pacotes nuget como uma única etapa para simplificar, a restauração está incluída em dotnet publish e, se falhar devido à falha do nuget, a mensagem de erro será legível.
Mas se você tiver muitas dependências nuget, você pode querer ter uma etapa separada, porque dessa forma o Docker a trata como uma camada distinta e a reutiliza se nenhum arquivo csproj for alterado, o que dá um tempo de compilação menor.
Assim, copiamos todos os projetos (porque o contexto de compilação agora é o diretório da solução) para o diretório /app dentro do contêiner.
Em seguida, em /app workdir, executamos o comando dotnet publish, especificando qual projeto compilar :
RUN dotnet publish PROJECT_NAME -c Release -o out
Aqui
PROJECT_NAME é o nome do diretório que contém o
arquivo .csproj
As demais instruções permanecem inalteradas com uma
pequena alteração - durante a cópia do aplicativo
compilado da imagem temporária, desta vez precisamos
passar o nome do projeto para o caminho:
COPY --from=build-env /app/PROJECT_NAME/out
.
Com isso podemos tratar os casos onde temos mais de um projeto na solução e suas dependências.
E estamos conversados...
"Porque a loucura de Deus é mais sábia do que os homens;
e a fraqueza de Deus é mais forte do que os homens."
1 Coríntios 1:25
Referências: