Tutorial: Colocar um aplicativo .NET em contêiner

Neste tutorial, você aprenderá como colocar em contêiner um aplicativo .NET com o Docker. Os contêineres têm muitos recursos e benefícios, como o de serem uma infraestrutura imutável, fornecer uma arquitetura portátil e permitir a escalabilidade. A imagem pode ser usada para criar contêineres para seu ambiente de desenvolvimento local, nuvem privada ou nuvem pública.

Neste tutorial, você:

  • Criar e publicar um aplicativo .NET simples
  • Criar e configurar um Dockerfile para o .NET
  • Compilar uma imagem do docker
  • Criar e executar um contêiner do Docker

Você entenderá as tarefas de build e implantação de contêiner do Docker para um aplicativo .NET. A plataforma Docker usa o Mecanismo do Docker para criar e empacotar aplicativos como imagens do Docker com agilidade. Essas imagens são gravadas no formato Dockerfile para serem implantadas e executadas em um contêiner em camadas.

Observação

Este tutorial não é para aplicativos ASP.NET Core. Se você estiver usando ASP.NET Core, consulte o tutorial Saiba como colocar em contêiner um tutorial do aplicativo ASP.NET Core.

Pré-requisitos

Instale os seguintes pré-requisitos:

  • SDK do .NET 8+
    Se você tiver o .NET instalado, use o comando dotnet --info para determinar qual SDK está usando.
  • Docker Community Edition
  • Uma pasta de trabalho temporária para o Dockerfile e o aplicativo de exemplo do .NET. Neste tutorial, o nome docker-working é usado como a pasta de trabalho.
  • SDK do .NET 7+
    Se você tiver o .NET instalado, use o comando dotnet --info para determinar qual SDK está usando.
  • Docker Community Edition
  • Uma pasta de trabalho temporária para o Dockerfile e o aplicativo de exemplo do .NET. Neste tutorial, o nome docker-working é usado como a pasta de trabalho.

Criar aplicativo .NET

Você precisa de um aplicativo .NET que o contêiner do Docker execute. Abra seu terminal, crie uma pasta de trabalho se você ainda não fez isso e entre nela. Na pasta de trabalho, execute o seguinte comando para criar um projeto em um subdiretório denominado App:

dotnet new console -o App -n DotNet.Docker

Sua árvore de pastas tem a seguinte aparência:

📁 docker-working
    └──📂 App
        ├──DotNet.Docker.csproj
        ├──Program.cs
        └──📂 obj
            ├── DotNet.Docker.csproj.nuget.dgspec.json
            ├── DotNet.Docker.csproj.nuget.g.props
            ├── DotNet.Docker.csproj.nuget.g.targets
            ├── project.assets.json
            └── project.nuget.cache

O comando dotnet new cria uma pasta chamada App e gera um aplicativo de console "Hello, World". Altere os diretórios e navegue até a pasta App a partir da sessão do terminal. Use o comando dotnet run para iniciar o aplicativo. O aplicativo é executado e imprime Hello World! abaixo do comando:

cd App
dotnet run
Hello World!

O modelo padrão cria um aplicativo que imprime no terminal e, em seguida, é encerrado imediatamente. Neste tutorial, você usará um aplicativo que faz um loop indefinidamente. Abra o arquivo Program.cs em um editor de texto.

Dica

Se você estiver usando Visual Studio Code, a partir da sessão do terminal anterior, digite o seguinte comando:

code .

Isso abrirá a pasta App que contém o projeto no Visual Studio Code.

O Program.cs deve ser como o seguinte código C#:

Console.WriteLine("Hello World!");

Substitua o arquivo pelo seguinte código que conta os números a cada segundo:

var counter = 0;
var max = args.Length is not 0 ? Convert.ToInt32(args[0]) : -1;
while (max is -1 || counter < max)
{
    Console.WriteLine($"Counter: {++counter}");
    await Task.Delay(TimeSpan.FromMilliseconds(1_000));
}
var counter = 0;
var max = args.Length is not 0 ? Convert.ToInt32(args[0]) : -1;
while (max is -1 || counter < max)
{
    Console.WriteLine($"Counter: {++counter}");
    await Task.Delay(TimeSpan.FromMilliseconds(1_000));
}

Salve o arquivo e teste o programa novamente com dotnet run. Lembre-se de que esse aplicativo é executado indefinidamente. Use o comando de cancelamento Ctrl+C para interrompê-lo. Confira o exemplo de saída abaixo:

dotnet run
Counter: 1
Counter: 2
Counter: 3
Counter: 4
^C

Se você passar um número na linha de comando para o aplicativo, ele apenas contará até tal valor e, em seguida, sairá. Experimente com dotnet run -- 5 para contar até cinco.

Importante

Quaisquer parâmetros após -- não são passados para o comando dotnet run e, em vez disso, são passados para o aplicativo.

Publicar aplicativo .NET

Antes de adicionar o aplicativo .NET à imagem do Docker, primeiro ele deve ser publicado. É melhor fazer com que o contêiner execute a versão publicada do aplicativo. Para publicar o aplicativo, execute o seguinte comando:

dotnet publish -c Release

Esse comando compila seu aplicativo para a pasta publish. O caminho para a pasta publish da pasta de trabalho deve ser .\App\bin\Release\net8.0\publish\.

Esse comando compila seu aplicativo para a pasta publish. O caminho para a pasta publish da pasta de trabalho deve ser .\App\bin\Release\net7.0\publish\.

Na pasta App, obtenha uma listagem de diretório da pasta de publicação para verificar se o arquivo DotNet.Docker.dll foi criado.

dir .\bin\Release\net8.0\publish\

    Directory: C:\Users\default\App\bin\Release\net8.0\publish

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
-a---           9/22/2023  9:17 AM            431 DotNet.Docker.deps.json
-a---           9/22/2023  9:17 AM           6144 DotNet.Docker.dll
-a---           9/22/2023  9:17 AM         157696 DotNet.Docker.exe
-a---           9/22/2023  9:17 AM          11688 DotNet.Docker.pdb
-a---           9/22/2023  9:17 AM            353 DotNet.Docker.runtimeconfig.json
dir .\bin\Release\net7.0\publish\

    Directory: C:\Users\default\App\bin\Release\net7.0\publish

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
-a---           2/13/2023  1:52 PM            431 DotNet.Docker.deps.json
-a---           2/13/2023  1:52 PM           6144 DotNet.Docker.dll
-a---           2/13/2023  1:52 PM         153600 DotNet.Docker.exe
-a---           2/13/2023  1:52 PM          11052 DotNet.Docker.pdb
-a---           2/13/2023  1:52 PM            253 DotNet.Docker.runtimeconfig.json

Criar o Dockerfile

O arquivo Dockerfile é usado pelo comando docker build para criar uma imagem de contêiner. Esse arquivo é um arquivo de texto chamado Dockerfile que não possui uma extensão.

Crie um arquivo chamado Dockerfile no diretório que contém .csproj e abra-o em um editor de texto. Este tutorial usa a imagem do runtime do ASP.NET Core (que contém a imagem do runtime do .NET) e corresponde ao aplicativo de console do .NET.

FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build-env
WORKDIR /App

# Copy everything
COPY . ./
# Restore as distinct layers
RUN dotnet restore
# Build and publish a release
RUN dotnet publish -c Release -o out

# Build runtime image
FROM mcr.microsoft.com/dotnet/aspnet:8.0
WORKDIR /App
COPY --from=build-env /App/out .
ENTRYPOINT ["dotnet", "DotNet.Docker.dll"]

Observação

A imagem ASP.NET Core runtime é usada intencionalmente aqui, embora a imagem mcr.microsoft.com/dotnet/runtime:8.0 poderia ter sido usada.

Dica

Esse Dockerfile usa builds de várias fases, o que otimiza o tamanho final da imagem, dispondo o build em camadas e deixando apenas os artefatos necessários. Para obter mais informações, consulte Documentação do Docker: builds de várias fases.

A palavra-chave FROM requer um nome de imagem de contêiner do Docker totalmente qualificado. O Registro de Contêiner da Microsoft (MCR, mcr.microsoft.com) é um sindicato do Docker Hub, que hospeda contêineres acessíveis ao público. O segmento dotnet é o repositório de contêineres, enquanto o segmento sdk ou aspnet é o nome da imagem do contêiner. A imagem é marcada com 8.0, que é usado para controle de versão. Portanto, mcr.microsoft.com/dotnet/aspnet:8.0 é o runtime do .NET 8.0. Verifique a execução da versão do runtime que corresponda ao runtime direcionado pelo seu SDK. Por exemplo, o aplicativo criado na seção anterior usou o SDK do .NET 8.0 e a imagem base referenciada no Dockerfile é marcada com 8.0.

Importante

Ao usar imagens de contêiner baseadas no Windows, você precisa especificar a marca de imagem além de simplesmente 8.0, por exemplo, mcr.microsoft.com/dotnet/aspnet:8.0-nanoserver-1809 em vez de mcr.microsoft.com/dotnet/aspnet:8.0. Selecione um nome de imagem com base no fato de você estar usando o Nano Server ou o Windows Server Core e a versão desse sistema operacional. Você pode encontrar uma lista completa de todas as marcas com suporte na Página Docker Hub do .NET.

Salve o arquivo Dockerfile. A estrutura de diretório da pasta de trabalho deve ser semelhante à mostrada a seguir. Algumas das pastas e arquivos de nível mais profundo foram omitidas para reduzir o tamanho do artigo:

📁 docker-working
    └──📂 App
        ├── Dockerfile
        ├── DotNet.Docker.csproj
        ├── Program.cs
        ├──📂 bin
        │   └──📂 Release
        │       └──📂 net8.0
        │           └──📂 publish
        │               ├── DotNet.Docker.deps.json
        │               ├── DotNet.Docker.exe
        │               ├── DotNet.Docker.dll
        │               ├── DotNet.Docker.pdb
        │               └── DotNet.Docker.runtimeconfig.json
        └──📁 obj
            └──...
FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build-env
WORKDIR /App

# Copy everything
COPY . ./
# Restore as distinct layers
RUN dotnet restore
# Build and publish a release
RUN dotnet publish -c Release -o out

# Build runtime image
FROM mcr.microsoft.com/dotnet/aspnet:7.0
WORKDIR /App
COPY --from=build-env /App/out .
ENTRYPOINT ["dotnet", "DotNet.Docker.dll"]

Observação

A imagem ASP.NET Core runtime é usada intencionalmente aqui, embora a imagem mcr.microsoft.com/dotnet/runtime:7.0 poderia ter sido usada.

Dica

Esse Dockerfile usa builds de várias fases, o que otimiza o tamanho final da imagem, dispondo o build em camadas e deixando apenas os artefatos necessários. Para obter mais informações, consulte Documentação do Docker: builds de várias fases.

A palavra-chave FROM requer um nome de imagem de contêiner do Docker totalmente qualificado. O Registro de Contêiner da Microsoft (MCR, mcr.microsoft.com) é um sindicato do Docker Hub, que hospeda contêineres acessíveis ao público. O segmento dotnet é o repositório de contêineres, enquanto o segmento sdk ou aspnet é o nome da imagem do contêiner. A imagem é marcada com 7.0, que é usado para controle de versão. Portanto, mcr.microsoft.com/dotnet/aspnet:7.0 é o runtime do .NET 7.0. Verifique a execução da versão do runtime que corresponda ao runtime direcionado pelo seu SDK. Por exemplo, o aplicativo criado na seção anterior usou o SDK do .NET 7.0 e a imagem base referenciada no Dockerfile é marcada com 7.0.

Salve o arquivo Dockerfile. A estrutura de diretório da pasta de trabalho deve ser semelhante à mostrada a seguir. Algumas das pastas e arquivos de nível mais profundo foram omitidas para reduzir o tamanho do artigo:

📁 docker-working
    └──📂 App
        ├── Dockerfile
        ├── DotNet.Docker.csproj
        ├── Program.cs
        ├──📂 bin
        │   └──📂 Release
        │       └──📂 net7.0
        │           └──📂 publish
        │               ├── DotNet.Docker.deps.json
        │               ├── DotNet.Docker.exe
        │               ├── DotNet.Docker.dll
        │               ├── DotNet.Docker.pdb
        │               └── DotNet.Docker.runtimeconfig.json
        └──📁 obj
            └──...

Do terminal, execute o seguinte comando:

docker build -t counter-image -f Dockerfile .

O Docker processará cada linha no Dockerfile. O . no comando docker build define o contexto de build da imagem. A opção -f é o caminho para o Dockerfile. Esse comando constrói a imagem e cria um repositório local chamado counter-image que aponta para essa imagem. Após a conclusão desse comando, execute docker images para ver uma lista de imagens instaladas:

docker images
REPOSITORY                         TAG       IMAGE ID       CREATED          SIZE
counter-image                      latest    2f15637dc1f6   10 minutes ago   217MB

O repositório counter-image é o nome da imagem. A marca latest é a marca usada para identificar a imagem. O 2f15637dc1f6 é a ID da imagem. O 10 minutes ago especifica a hora em que a imagem foi criada. O 217MB é tamanho da imagem. As etapas finais do Dockerfile são criar um contêiner da imagem e executar o aplicativo, copiar o aplicativo publicado para o contêiner e definir o ponto de entrada.

FROM mcr.microsoft.com/dotnet/aspnet:8.0
WORKDIR /App
COPY --from=build-env /App/out .
ENTRYPOINT ["dotnet", "DotNet.Docker.dll"]
docker images
REPOSITORY                         TAG       IMAGE ID       CREATED          SIZE
counter-image                      latest    2f15637dc1f6   10 minutes ago   208MB

O repositório counter-image é o nome da imagem. A marca latest é a marca usada para identificar a imagem. O 2f15637dc1f6 é a ID da imagem. O 10 minutes ago especifica a hora em que a imagem foi criada. O 208MB é tamanho da imagem. As etapas finais do Dockerfile são criar um contêiner da imagem e executar o aplicativo, copiar o aplicativo publicado para o contêiner e definir o ponto de entrada.

FROM mcr.microsoft.com/dotnet/aspnet:7.0
WORKDIR /App
COPY --from=build-env /App/out .
ENTRYPOINT ["dotnet", "DotNet.Docker.dll"]

O comando COPY informa ao Docker para copiar a pasta especificada em seu computador para uma pasta no contêiner. Nesse exemplo, a pasta publish é copiada para uma pasta chamada App/out no contêiner.

O comando WORKDIR altera o diretório atual dentro do contêiner para App.

O comando seguinte, ENTRYPOINT, informa ao Docker para configurar o contêiner para ser executado como um executável. Quando o contêiner é iniciado, o comando ENTRYPOINT é executado. Quando esse comando terminar, o contêiner será interrompido automaticamente.

Dica

Antes do .NET 8, os contêineres configurados para serem executados como somente leitura podiam falhar com Failed to create CoreCLR, HRESULT: 0x8007000E. Para resolver esse problema, especifique uma variável de ambiente DOTNET_EnableDiagnostics como 0 (pouco antes da etapa ENTRYPOINT):

ENV DOTNET_EnableDiagnostics=0

Para obter mais informações sobre diversas variáveis de ambiente, confira Variáveis de ambiente do .NET.

Observação

O .NET 6 usa o prefixo DOTNET_ como padrão em vez de COMPlus_ para variáveis de ambiente que configuram o comportamento de tempo de execução do .NET. No entanto, o prefixo COMPlus_ continuará funcionando. Se você estiver usando uma versão anterior do runtime do .NET, continue usando o prefixo COMPlus_ para variáveis de ambiente.

Criar um contêiner

Agora que você tem uma imagem que contém o seu aplicativo, você pode criar um contêiner. Você pode criar um contêiner de duas maneiras. Primeiro, criar um novo contêiner que foi interrompido.

docker create --name core-counter counter-image

Esse comando docker create cria um contêiner com base na imagem counter-image. A saída desse comando mostra a ID DO CONTÊINER (a sua será diferente) do contêiner criado:

d0be06126f7db6dd1cee369d911262a353c9b7fb4829a0c11b4b2eb7b2d429cf

Para ver uma lista de todos os contêineres, use o comando docker ps -a:

docker ps -a
CONTAINER ID   IMAGE           COMMAND                  CREATED          STATUS    PORTS     NAMES
d0be06126f7d   counter-image   "dotnet DotNet.Docke…"   12 seconds ago   Created             core-counter

Gerenciar o contêiner

O contêiner foi criado com um nome core-counter específico, que é usado para gerenciar o contêiner. O exemplo a seguir usa o comando docker start para iniciar o contêiner e, em seguida, usa o comando docker ps para mostrar apenas os contêineres em execução:

docker start core-counter
core-counter

docker ps
CONTAINER ID   IMAGE           COMMAND                  CREATED          STATUS          PORTS     NAMES
cf01364df453   counter-image   "dotnet DotNet.Docke…"   53 seconds ago   Up 10 seconds             core-counter

Da mesma forma, o comando docker stop interrompe o contêiner. O exemplo a seguir usa o comando docker stop para interromper o contêiner e, em seguida, usa o comando docker ps para mostrar que nenhum contêiner está em execução:

docker stop core-counter
core-counter

docker ps
CONTAINER ID    IMAGE    COMMAND    CREATED    STATUS    PORTS    NAMES

Conectar-se a um contêiner

Depois que um contêiner estiver em execução, você poderá se conectar a ele para ver a saída. Use os comandos docker start e docker attach para iniciar o contêiner e inspecionar o fluxo de saída. Neste exemplo, pressionar Ctrl+C desanexa do contêiner em execução. Ao pressionar a tecla, o processo no contêiner é encerrado, a menos que especificado de outra forma, o que interromperia o contêiner. O parâmetro --sig-proxy=false garante que Ctrl+C não interrompa o processo no contêiner.

Depois de desanexar do contêiner, reanexe para verificar se ele ainda está em execução e contando.

docker start core-counter
core-counter

docker attach --sig-proxy=false core-counter
Counter: 7
Counter: 8
Counter: 9
^C

docker attach --sig-proxy=false core-counter
Counter: 17
Counter: 18
Counter: 19
^C

Excluir um contêiner

Para este artigo, você não quer contêineres sem utilidade. Exclua o contêiner que você criou anteriormente. Se o contêiner estiver em execução, interrompa-o.

docker stop core-counter

O exemplo a seguir lista todos os contêineres. Em seguida, ele usa o comando docker rm para excluir o contêiner e, em depois, verifica uma segunda vez para verificar qualquer contêiner em execução.

docker ps -a
CONTAINER ID    IMAGE            COMMAND                   CREATED          STATUS                        PORTS    NAMES
2f6424a7ddce    counter-image    "dotnet DotNet.Dock…"    7 minutes ago    Exited (143) 20 seconds ago            core-counter

docker rm core-counter
core-counter

docker ps -a
CONTAINER ID    IMAGE    COMMAND    CREATED    STATUS    PORTS    NAMES

Execução única

O Docker fornece o comando docker run para criar e executar o contêiner como um único comando. Este comando elimina a necessidade de executar docker create e, em seguida, docker start. Você também pode definir esse comando para excluir automaticamente o contêiner quando o contêiner for interrompido. Por exemplo, use docker run -it --rm para fazer duas coisas: primeiro, use automaticamente o terminal atual para se conectar ao contêiner e, quando o contêiner terminar, remova-o:

docker run -it --rm counter-image
Counter: 1
Counter: 2
Counter: 3
Counter: 4
Counter: 5
^C

O contêiner também passa parâmetros para a execução do aplicativo .NET. Para instruir o aplicativo .NET a contar apenas até três, passe em 3.

docker run -it --rm counter-image 3
Counter: 1
Counter: 2
Counter: 3

Com docker run -it, o comando Ctrl+C interrompe o processo em execução no contêiner, que, por sua vez, interrompe o contêiner. Como o parâmetro --rm foi fornecido, o contêiner é automaticamente excluído quando o processo é interrompido. Verifique se ele não existe:

docker ps -a
CONTAINER ID    IMAGE    COMMAND    CREATED    STATUS    PORTS    NAMES

Alterar o ENTRYPOINT

O comando docker run também permite modificar o comando ENTRYPOINT do Dockerfile e executar outra coisa, mas apenas para esse contêiner. Por exemplo, use o seguinte comando para executar bash ou cmd.exe. Edite o comando conforme necessário.

Neste exemplo, ENTRYPOINT é alterado para cmd.exe. Ctrl+C é pressionado para finalizar o processo e interromper o contêiner.

docker run -it --rm --entrypoint "cmd.exe" counter-image

Microsoft Windows [Version 10.0.17763.379]
(c) 2018 Microsoft Corporation. All rights reserved.

C:\>dir
 Volume in drive C has no label.
 Volume Serial Number is 3005-1E84

 Directory of C:\

04/09/2019  08:46 AM    <DIR>          app
03/07/2019  10:25 AM             5,510 License.txt
04/02/2019  01:35 PM    <DIR>          Program Files
04/09/2019  01:06 PM    <DIR>          Users
04/02/2019  01:35 PM    <DIR>          Windows
               1 File(s)          5,510 bytes
               4 Dir(s)  21,246,517,248 bytes free

C:\>^C

Comandos essenciais

O Docker tem muitos comandos diferentes que criam, gerenciam e interagem com contêineres e imagens. Esses comandos do Docker são essenciais para gerenciar seus contêineres:

Limpar os recursos

Durante este tutorial, você criou contêineres e imagens. Se quiser, exclua esses recursos. Use os seguintes comandos para:

  1. Listar todos os contêineres

    docker ps -a
    
  2. Interrompa os contêineres que estão em execução pelo nome.

    docker stop core-counter
    
  3. Excluir o contêiner

    docker rm core-counter
    

Em seguida, exclua todas as imagens que você não deseja mais em seu computador. Exclua a imagem criada pelo seu Dockerfile e exclua a imagem do .NET na qual o Dockerfile teve base. Você pode usar a ID DA IMAGEM ou a cadeia de caracteres formatada REPOSITÓRIO:TAG.

docker rmi counter-image:latest
docker rmi mcr.microsoft.com/dotnet/aspnet:8.0
docker rmi counter-image:latest
docker rmi mcr.microsoft.com/dotnet/aspnet:7.0

Use o comando docker images para ver uma lista de imagens instaladas.

Dica

Arquivos de imagem podem ser grandes. Normalmente, você removeria contêineres temporários criados durante o teste e o desenvolvimento de seu aplicativo. Em geral, mantenha as imagens de base com o runtime instalado se você planeja construir outras imagens com base nesse runtime.

Próximas etapas