Esercitazione: App .Net in contenitori

In questa esercitazione si apprenderà come inserire in contenitori un'applicazione .NET con Docker. I contenitori hanno molte funzionalità e vantaggi, ad esempio un'infrastruttura non modificabile, offrono un'architettura portabile e consentono la scalabilità. L'immagine può essere usata per creare contenitori per l'ambiente di sviluppo locale, il cloud privato o il cloud pubblico.

In questa esercitazione:

  • Creare e pubblicare una semplice app .NET
  • Creare e configurare un Dockerfile per .NET
  • Creare un'immagine Docker
  • Creare ed eseguire un contenitore Docker

Si comprenderanno le attività di compilazione e distribuzione del contenitore Docker per un'applicazione .NET. La piattaforma Docker usa il motore Docker per compilare rapidamente app e inserirle in pacchetti come immagini Docker. Queste immagini vengono scritte nel formato Dockerfile per la distribuzione e l'esecuzione in un contenitore su più livelli.

Nota

Questa esercitazione non è destinata alle app core di ASP.NET. Se si usa ASP.NET Core, vedere l'esercitazione Informazioni su come inserire in contenitori un'applicazione ASP.NET Core.

Prerequisiti

Installare i prerequisiti seguenti:

  • .NET 8+ SDK
    Se è installato .NET, usare il comando dotnet --info per determinare l'SDK in uso.
  • Docker Community Edition
  • Cartella di lavoro temporanea per l'app di esempio Dockerfile e .NET. In questa esercitazione il nome docker-working viene usato come cartella di lavoro.
  • .NET 7+ SDK
    Se è installato .NET, usare il comando dotnet --info per determinare l'SDK in uso.
  • Docker Community Edition
  • Cartella di lavoro temporanea per l'app di esempio Dockerfile e .NET. In questa esercitazione il nome docker-working viene usato come cartella di lavoro.

Creare un'app .NET

È necessaria un'app .NET eseguita dal contenitore Docker. Aprire il terminale in uso, creare una cartella di lavoro se non è già stata creata e passare alla cartella. Nella cartella di lavoro eseguire il comando seguente per creare un nuovo progetto in una sottodirectory denominata App:

dotnet new console -o App -n DotNet.Docker

L'albero delle cartelle è simile al seguente:

📁 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

Il comando dotnet new crea una nuova cartella denominata App e genera un'applicazione console "Hello World". Modificare le directory e passare alla cartella App dalla sessione del terminale. Usare il comando dotnet run per avviare l'app. L'applicazione viene eseguita e stampa Hello World! sotto il comando:

cd App
dotnet run
Hello World!

Il modello predefinito crea un'app che stampa sul terminale e quindi termina immediatamente. Per questa esercitazione si usa un'app che esegue un ciclo illimitato. Aprire il file Program.cs in un editor di testo.

Suggerimento

Se si usa Visual Studio Code, dalla sessione del terminale precedente digitare il comando seguente:

code .

Verrà aperta la cartella App che contiene il progetto in Visual Studio Code.

Il Program.cs dovrebbe essere simile al codice C# seguente:

Console.WriteLine("Hello World!");

Sostituirlo con il codice seguente che conta i numeri ogni secondo:

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));
}

Salvare il file e testare di nuovo il programma con dotnet run. Tenere presente che l'app viene eseguita per un tempo illimitato. Usare il comando di annullamento CTRL+C per arrestarla. Di seguito è riportato un esempio di output:

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

Se si passa un numero all'app nella riga di comando, verrà eseguito il conteggio fino a quel numero e quindi l'app verrà chiusa. Provare con dotnet run -- 5 per contare fino a cinque.

Importante

I parametri dopo -- non vengono passati al comando dotnet run e vengono invece passati all'applicazione.

Pubblicare un'app .NET

Prima di aggiungere l'app .NET all'immagine Docker, è necessario pubblicarla. È consigliabile che il contenitore esegua la versione pubblicata dell'app. Per pubblicare l'app, eseguire il comando seguente:

dotnet publish -c Release

Questo comando compila l'app nella cartella publish. Il percorso della cartella publish dalla cartella di lavoro deve essere .\App\bin\Release\net8.0\publish\.

Questo comando compila l'app nella cartella publish. Il percorso della cartella publish dalla cartella di lavoro deve essere .\App\bin\Release\net7.0\publish\.

Dalla cartella App ottenere un elenco di directory della cartella publish per verificare che il file DotNet.Docker.dll sia stato creato.

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

Creare il Dockerfile

Il file Dockerfile viene usato dal comando docker build per creare un'immagine del contenitore. Questo file è un file di testo denominato Dockerfile che non ha un'estensione.

Creare un file denominato Dockerfile nella directory contenente il file con estensione csproj e aprirlo in un editor di testo. Questa esercitazione usa l'immagine di runtime di ASP.NET Core (che contiene l'immagine di runtime .NET) e corrisponde all'applicazione console .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"]

Nota

L'immagine di runtime di ASP.NET Core viene usata intenzionalmente qui, anche se l'immagine mcr.microsoft.com/dotnet/runtime:8.0 potrebbe essere stata usata.

Suggerimento

Questo Dockerfile usa compilazioni a più fasi, che ottimizzano le dimensioni finali dell'immagine tramite il layer della compilazione e lasciando solo gli artefatti necessari. Per altre informazioni, vedere Docker Docs: compilazioni a più fasi.

La parola chiave FROM richiede un nome completo dell'immagine del contenitore Docker. Microsoft Container Registry (MCR, mcr.microsoft.com) è un syndicate dell'hub Docker, che ospita contenitori accessibili pubblicamente. Il segmento dotnet è il repository di contenitori, mentre il segmento sdk o aspnet è il nome dell'immagine del contenitore. L'immagine è contrassegnata con 8.0, che viene usata per il controllo delle versioni. Pertanto, mcr.microsoft.com/dotnet/aspnet:8.0 è il runtime di .NET 8.0. Assicurarsi di eseguire il pull della versione di runtime corrispondente al runtime di destinazione dell'SDK. Ad esempio, l'app creata nella sezione precedente usa .NET 8.0 SDK e l'immagine di base a cui si fa riferimento nel Dockerfile è contrassegnata con 8.0.

Importante

Quando si usano immagini contenitore basate su Windows, è necessario specificare il tag di immagine oltre semplicemente 8.0, ad esempio, mcr.microsoft.com/dotnet/aspnet:8.0-nanoserver-1809 anziché mcr.microsoft.com/dotnet/aspnet:8.0. Selezionare il nome dell'immagine in base all'utilizzo di Nano Server o Windows Server Core e alla versione del sistema operativo. È possibile trovare un elenco completo di tutti i tag supportati nella pagina dell'hub Docker di .NET.

Salvare il file Dockerfile. La struttura directory della cartella di lavoro deve avere un aspetto simile al seguente. Alcuni file e cartelle di livello più approfondito sono stati omessi per risparmiare spazio nell'articolo:

📁 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"]

Nota

L'immagine di runtime ASP.NET Core viene usata intenzionalmente qui, anche se l'immagine mcr.microsoft.com/dotnet/runtime:7.0 potrebbe essere stata usata.

Suggerimento

Questo Dockerfile usa compilazioni a più fasi, che ottimizzano le dimensioni finali dell'immagine tramite il layer della compilazione e lasciando solo gli artefatti necessari. Per altre informazioni, vedere Docker Docs: compilazioni a più fasi.

La parola chiave FROM richiede un nome completo dell'immagine del contenitore Docker. Il Registro Contenitori Microsoft (MCR, mcr.microsoft.com) è un syndicate dell'hub Docker, che ospita contenitori accessibili pubblicamente. Il segmento dotnet è il repository di contenitori, mentre il segmento sdk o aspnet è il nome dell'immagine del contenitore. L'immagine è contrassegnata con 7.0, che viene usata per il controllo delle versioni. Pertanto, mcr.microsoft.com/dotnet/aspnet:7.0 è il runtime di .NET 7.0. Assicurarsi di eseguire il pull della versione di runtime corrispondente al runtime di destinazione dell'SDK. Ad esempio, l'app creata nella sezione precedente ha usato .NET 7.0 SDK e l'immagine di base a cui si fa riferimento nel Dockerfile è contrassegnata con 7.0.

Salvare il file Dockerfile. La struttura directory della cartella di lavoro deve avere un aspetto simile al seguente. Alcuni file e cartelle di livello più approfondito sono stati omessi per risparmiare spazio nell'articolo:

📁 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
            └──...

Dal terminale eseguire il comando seguente:

docker build -t counter-image -f Dockerfile .

Docker elaborerà ogni riga nel Dockerfile. . nel comando docker build imposta il contesto di compilazione dell'immagine. L'opzione -f è il percorso del Dockerfile. Questo comando compila l'immagine e crea un repository locale denominato counter-image che punta a tale immagine. Dopo l'esecuzione del comando, eseguire docker images per visualizzare un elenco delle immagini installate:

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

Il repository counter-image è il nome dell'immagine. Il tag latest è il tag usato per identificare l'immagine. 2f15637dc1f6 è l'ID immagine. 10 minutes ago è l'ora di creazione dell'immagine. 217MB è la dimensione dell'immagine. I passaggi finali del Dockerfile sono creare un contenitore dall'immagine ed eseguire l'app, copiare l'app pubblicata nel contenitore e definire il punto di ingresso.

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

Il repository counter-image è il nome dell'immagine. Il tag latest è il tag usato per identificare l'immagine. 2f15637dc1f6 è l'ID immagine. 10 minutes ago è l'ora di creazione dell'immagine. 208MB è la dimensione dell'immagine. I passaggi finali del Dockerfile sono creare un contenitore dall'immagine ed eseguire l'app, copiare l'app pubblicata nel contenitore e definire il punto di ingresso.

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

Il comando COPY indica a Docker di copiare la cartella specificata nel computer in una cartella nel contenitore. In questo esempio, la cartella publish viene copiata in una cartella denominata app/out nel contenitore.

Il comando WORKDIR modifica la directory corrente all'interno del contenitore in App.

Il comando successivo, ENTRYPOINT, indica a Docker di configurare il contenitore in modo che venga eseguito come un eseguibile. All'avvio del contenitore viene eseguito il comando ENTRYPOINT. Al termine del comando, il contenitore si arresta automaticamente.

Suggerimento

Prima di .NET 8, i contenitori configurati per l'esecuzione come di sola lettura potrebbero non riuscire con Failed to create CoreCLR, HRESULT: 0x8007000E. Per risolvere questo problema, specificare una variabile di ambiente DOTNET_EnableDiagnostics come 0 (subito prima del passaggio ENTRYPOINT):

ENV DOTNET_EnableDiagnostics=0

Per altre informazioni sulle diverse variabili di ambiente .NET, vedere Variabili di ambiente .NET.

Nota

.NET 6 standardizza il prefisso DOTNET_ anziché COMPlus_ per le variabili di ambiente che configurano il comportamento di runtime di .NET. Tuttavia, il prefisso COMPlus_ continuerà a funzionare. Se si usa una versione precedente del runtime .NET, è comunque consigliabile usare il prefisso COMPlus_ per le variabili di ambiente.

Creazione di un contenitore

Ora che si dispone di un'immagine che contiene l'app, è possibile creare un contenitore. Lo si può fare in due modi. Prima di tutto, creare un nuovo contenitore arrestato.

docker create --name core-counter counter-image

Questo comando docker create crea un contenitore basato sull'immagine del contatore. L'output di questo comando mostra l'ID CONTENITORE (il contenitore sarà diverso) del contenitore creato:

d0be06126f7db6dd1cee369d911262a353c9b7fb4829a0c11b4b2eb7b2d429cf

Per visualizzare un elenco di tutti i contenitori, usare il 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

Gestire il contenitore

Il contenitore è stato creato con un nome specifico core-counter, questo nome viene usato per gestire il contenitore. L'esempio seguente usa il comando docker start per avviare il contenitore e quindi usa il comando docker ps per visualizzare solo i contenitori in esecuzione:

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

Analogamente, il comando docker stop arresta il contenitore. L'esempio seguente usa il comando docker stop per arrestare il contenitore e quindi usa il comando docker ps per mostrare che non è in esecuzione alcun contenitore:

docker stop core-counter
core-counter

docker ps
CONTAINER ID    IMAGE    COMMAND    CREATED    STATUS    PORTS    NAMES

Connettersi a un contenitore

Quando un contenitore è in esecuzione, è possibile connettersi ad esso per visualizzare l'output. Usare i comandi docker start e docker attach per avviare il contenitore e osservare il flusso di output. In questo esempio viene usata la sequenza di tasti CTRL+C per disconnettersi dal contenitore in esecuzione. Questa sequenza di tasti termina il processo nel contenitore, a meno che non sia specificato diversamente, che arresta il contenitore. Il parametro --sig-proxy=false garantisce che CTRL+C non arresti il processo nel contenitore.

Dopo essersi scollegati dal contenitore, collegarsi di nuovo per verificare che sia ancora in esecuzione e continui a eseguire il conteggio.

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

Eliminare un contenitore

Per questo articolo, non si vogliono contenitori che non fanno nulla. Eliminare il contenitore creato in precedenza. Se è in esecuzione, arrestarlo.

docker stop core-counter

L'esempio seguente elenca tutti i contenitori. Usare quindi il comando docker rm per eliminare il contenitore e quindi controllare una seconda volta per eventuali contenitori in esecuzione.

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

Esecuzione singola

Docker fornisce il comando docker run per creare ed eseguire il contenitore con un unico comando. Questo comando elimina la necessità di eseguire docker create e quindi docker start. È anche possibile impostare il comando in modo che elimini automaticamente il contenitore quando viene arrestato. Ad esempio, usare docker run -it --rm per eseguire due operazioni, ossia usare automaticamente il terminale corrente per connettersi al contenitore e quindi, al termine dell'esecuzione, rimuovere il contenitore:

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

Il contenitore passa anche i parametri all'esecuzione dell'app .NET. Per indicare all'app .NET di contarne solo tre, passare a 3.

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

Con docker run -it, il comando CTRL+C arresta il processo in esecuzione nel contenitore, che a sua volta arresta il contenitore. Poiché è stato specificato il parametro --rm, il contenitore viene eliminato automaticamente quando viene arrestato il processo. Verificare che non esista:

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

Modificare il comando ENTRYPOINT

Il comando docker run consente anche di modificare il comando ENTRYPOINT dal Dockerfile e di eseguire qualcosa di diverso, ma solo per il contenitore in questione. Ad esempio, usare il comando seguente per eseguire bash o cmd.exe. Modificare il comando in base alle esigenze.

In questo esempio ENTRYPOINT viene sostituito con cmd.exe. Si preme quindi CTRL+C per terminare il processo e arrestare il contenitore.

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

Comandi essenziali

Docker include molti comandi diversi che creano, gestiscono e interagiscono con contenitori e immagini. I comandi Docker seguenti sono essenziali per la gestione dei contenitori:

Pulire le risorse

Durante questa esercitazione sono stati creati contenitori e immagini. Se si preferisce, è possibile eliminare queste risorse. Usare i comandi seguenti per

  1. Elencare tutti i contenitori

    docker ps -a
    
  2. Arrestare i contenitori in esecuzione in base al nome.

    docker stop core-counter
    
  3. Eliminare il contenitore

    docker rm core-counter
    

Eliminare quindi le immagini che non si desidera conservare nel computer. Eliminare l'immagine creata dal Dockerfile e quindi eliminare l'immagine .NET su cui si basava il Dockerfile. È possibile usare l'IMAGE ID o la stringa formattata come REPOSITORY: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

Usare il comando docker images per visualizzare un elenco delle immagini installate.

Suggerimento

I file di immagine possono essere di grandi dimensioni. In genere è consigliabile rimuovere i contenitori temporanei creati durante il test e lo sviluppo dell'app. Conservare invece le immagini di base con il runtime installato se si prevede di compilare altre immagini basate su quel runtime.

Passaggi successivi