Docker - Dockerfile com mais de um projeto

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

FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build-env
WORKDIR /app

# Copia csproj e restaura as camadas
COPY *.csproj ./
RUN dotnet restore

# Copy tudo e dá um build
COPY . ./
RUN dotnet publish -c Release -o out

# Build a imagem runtime
FROM mcr.microsoft.com/dotnet/aspnet:6.0
WORKDIR /app
COPY --from=build-env /app/out .
ENTRYPOINT
["dotnet", "aspnetapp.dll"]

E os comandos usados para criar a imagem a partir deste arquivo Dockerfile:


docker build -t aspnetapp .
docker run -d -p 8080:80 --name myapp aspnetapp

 

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:

FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build-env
WORKDIR /app

COPY . ./
RUN dotnet publish PROJECT_NAME -c Release -o out

FROM mcr.microsoft.com/dotnet/aspnet:6.0
WORKDIR /app
COPY --from=build-env /app/PROJECT_NAME/out .

ENTRYPOINT
["dotnet", "PROJECT_NAME.dll"]

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:


José Carlos Macoratti