Condividi tramite


Esercitazione: Containerizzare un'app .NET

Questa esercitazione illustra 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, tu:

  • 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 esplorano le attività di compilazione e distribuzione del contenitore Docker per un'applicazione .NET. La piattaforma Docker usa il motore Docker per creare e impacchettare rapidamente app come immagini Docker . Queste immagini vengono scritte nel formato Dockerfile da distribuire ed eseguire in un contenitore a più livelli.

Suggerimento

Se sei interessato a pubblicare la tua app .NET come contenitore senza dover usare Docker o Podman, consulta Creare un contenitore per un'app .NET con dotnet publish.

Nota

Questa esercitazione non è per le app ASP.NET Core. Se utilizzi ASP.NET Core, consulta il tutorial Scopri come containerizzare un'applicazione ASP.NET Core.

Prerequisiti

Installare i prerequisiti seguenti:

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

Creare un'app .NET

È necessaria un'app .NET eseguita dal contenitore Docker. Apri il terminale, crea una cartella di lavoro se non l'hai già fatto, ed entra al suo interno. 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 alla struttura di directory 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". Ora, cambi directory e ti sposti nella 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 .

Questo comando apre 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!");

Sostituire il file 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 questa app viene eseguita per un periodo illimitato. Usare il comando cancel CTRL+C per arrestarlo. Si consideri l'output di esempio seguente:

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

Se passi un numero nella riga di comando all'applicazione, limita il conteggio a tale numero e quindi esce. Prova con dotnet run -- 5 per contare fino a cinque.

Importante

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

Pubblicare un'app .NET

Affinché l'app sia adatta per la creazione di un'immagine, deve essere compilata. Il comando dotnet publish è più adatto a questo, perché compila e pubblica l'app. Per informazioni di riferimento approfondite, vedere documentazione sui comandi dotnet build e dotnet publish.

dotnet publish -c Release

Mancia

Se sei interessato a pubblicare la tua app .NET come contenitore senza la necessità di Docker, vedere Containerizzare un'app .NET con dotnet publish.

Il comando compila l'app nella cartella publish . Il percorso della cartella di pubblicazione dalla cartella di lavoro deve essere ./App/bin/Release/TFM/publish/:

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

dir .\bin\Release\net9.0\publish\

    Directory: C:\Users\default\docker-working\App\bin\Release\net9.0\publish

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
-a----          1/6/2025  10:11 AM            431 DotNet.Docker.deps.json
-a----          1/6/2025  10:11 AM           6144 DotNet.Docker.dll
-a----          1/6/2025  10:11 AM         145408 DotNet.Docker.exe
-a----          1/6/2025  10:11 AM          11716 DotNet.Docker.pdb
-a----          1/6/2025  10:11 AM            340 DotNet.Docker.runtimeconfig.json
dir .\bin\Release\net8.0\publish\

    Directory: C:\Users\default\docker-working\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

Creare il Dockerfile

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

Creare un file denominato Dockerfile nella directory contenente il .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:9.0@sha256:3fcf6f1e809c0553f9feb222369f58749af314af6f063f389cbd2f913b4ad556 AS build
WORKDIR /App

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

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

Nota

L'immagine di runtime ASP.NET Core viene usata intenzionalmente in questo caso, anche se è possibile usare l'immagine mcr.microsoft.com/dotnet/runtime:9.0.

FROM mcr.microsoft.com/dotnet/sdk:8.0@sha256:35792ea4ad1db051981f62b313f1be3b46b1f45cadbaa3c288cd0d3056eefb83 AS build
WORKDIR /App

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

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

Nota

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

Importante

L'inclusione di un algoritmo hash sicuro (SHA) dopo il tag immagine in un Dockerfile è una procedura consigliata. In questo modo si garantisce che l'immagine non venga manomessa e che l'immagine sia identica a quella prevista. SHA è un identificatore univoco per l'immagine. Per altre informazioni, vedere Docker Docs: Eseguire il pull di un'immagine in base al digest.

Mancia

Questo Dockerfile utilizza build a più stadi, che ottimizzano le dimensioni finali dell'immagine stratificando il processo di build e conservando solamente 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 sindacato dell'hub Docker, che ospita contenitori accessibili pubblicamente. Il segmento dotnet è il repository dei contenitori, mentre il segmento sdk o aspnet è il nome dell'immagine del container. L'immagine viene contrassegnata con 9.0, che viene usata per il controllo delle versioni. Di conseguenza, mcr.microsoft.com/dotnet/aspnet:9.0 è il runtime di .NET 9.0. Accertati di ottenere la versione di runtime che corrisponde al runtime a cui è destinato l'SDK. Ad esempio, l'app creata nella sezione precedente usa .NET 9.0 SDK e l'immagine di base a cui si fa riferimento nella Dockerfile viene contrassegnata con 9.0.

Importante

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

Salva il file Dockerfile. La struttura di directory della cartella di lavoro dovrebbe essere simile alla seguente. Alcuni file e cartelle di livello più approfondito vengono omessi per risparmiare spazio nell'articolo:

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

La parola chiave FROM richiede un nome completamente qualificato dell'immagine del contenitore Docker. Microsoft Container Registry (MCR, mcr.microsoft.com) è un sindacato 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 viene contrassegnata con 8.0, che viene usata per il controllo delle versioni. Di conseguenza, mcr.microsoft.com/dotnet/aspnet:8.0 è il runtime di .NET 8.0. Assicurati di scaricare la versione di runtime corrispondente al runtime mirato dal tuo SDK. Ad esempio, l'app creata nella sezione precedente usa .NET 8.0 SDK e l'immagine di base a cui si fa riferimento nella Dockerfile viene contrassegnata con 8.0.

Importante

Quando si usano immagini del 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 un nome di immagine in base al fatto che si usi Nano Server o Windows Server Core e quale versione del sistema operativo. È possibile trovare un elenco completo di tutti i tag supportati sulla pagina dell'hub Docker di .NET .

Salvare il file Dockerfile. La struttura di directory della cartella di lavoro dovrebbe essere simile alla seguente. Alcuni file e cartelle di livello più approfondito vengono 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
            └──...

L'istruzione ENTRYPOINT imposta dotnet come host per l'DotNet.Docker.dll. Tuttavia, è possibile invece definire il ENTRYPOINT come l'eseguibile dell'app stessa, facendo affidamento sul sistema operativo come host dell'applicazione.

ENTRYPOINT ["./DotNet.Docker"]

In questo modo l'app viene eseguita direttamente, senza dotnete si basa invece sull'host dell'app e sul sistema operativo sottostante. Per altre informazioni sulla distribuzione di file binari multipiattaforma, vedere Produrre un file binario multipiattaforma.

Per compilare il contenitore, eseguire il comando seguente dal terminale:

docker build -t counter-image -f Dockerfile .

Docker elabora ogni riga nel Dockerfile . Il . nel comando docker build imposta il contesto di compilazione dell'immagine. Il commutatore -f è il percorso al Dockerfile . Questo comando costruisce l'immagine e crea un repository locale denominato counter-image che punta a tale immagine. Al termine di questo comando, eseguire docker images per visualizzare un elenco di immagini installate:

REPOSITORY       TAG       IMAGE ID       CREATED          SIZE
counter-image    latest    1c1f1433e51d   32 seconds ago   223MB
docker images
REPOSITORY       TAG       IMAGE ID       CREATED          SIZE
counter-image    latest    2f15637dc1f6   10 minutes ago   217MB

Il repository counter-image è il nome dell'immagine. Inoltre, il tag immagine, l'identificatore dell'immagine, le dimensioni e quando è stato creato fanno parte dell'output. I passaggi finali del Dockerfile consentono di 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:9.0
WORKDIR /App
COPY --from=build /App/out .
ENTRYPOINT ["dotnet", "DotNet.Docker.dll"]
FROM mcr.microsoft.com/dotnet/aspnet:8.0
WORKDIR /App
COPY --from=build /App/out .
ENTRYPOINT ["dotnet", "DotNet.Docker.dll"]

Il comando FROM specifica l'immagine di base e il tag da usare. Il comando WORKDIR modifica la directory corrente all'interno del contenitore in App.

Il comando COPY indica a Docker di copiare la directory di origine specificata in una cartella di destinazione. In questo esempio, i pubblicano contenuto nel livello build vengono restituiti nella cartella denominata app/out , quindi è l'origine da cui copiare. Tutti i contenuti pubblicati nella directory App/out vengono copiati nella directory di lavoro corrente (App).

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

Mancia

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

ENV DOTNET_EnableDiagnostics=0

Per ulteriori 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.

Creare un contenitore

Dopo aver creato un'immagine che contiene l'app, è possibile creare un contenitore. È possibile creare un contenitore in due modi. Prima, crea un nuovo contenitore che sia fermo.

docker create --name core-counter counter-image

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

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 indicare che non sono in esecuzione contenitori:

docker stop core-counter
core-counter

docker ps
CONTAINER ID    IMAGE    COMMAND    CREATED    STATUS    PORTS    NAMES

Connettersi a un contenitore

Dopo l'esecuzione di un contenitore, è possibile connettersi a esso per visualizzare l'output. Usare i comandi docker start e docker attach per avviare il contenitore e visualizzare il flusso di output. In questo esempio, la sequenza di tasti Ctrl+C viene usata per scollegare il contenitore in esecuzione. Questa sequenza di tasti termina il processo nel contenitore a meno che non sia specificato diversamente; in tal caso, il contenitore verrà arrestato. Il parametro --sig-proxy=false garantisce che Ctrl+C non arresti il processo nel contenitore.

Dopo la disconnessione dal contenitore, ricollegare per verificare che sia ancora in esecuzione e 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 vuoi che i contenitori restino inattivi. Eliminare il contenitore creato in precedenza. Se il contenitore è in esecuzione, fermalo.

docker stop core-counter

Nell'esempio seguente sono elencati tutti i contenitori. Usa quindi il comando docker rm per eliminare il contenitore e quindi controlla 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 come singolo comando. Questo comando elimina la necessità di eseguire docker create e quindi docker start. È anche possibile impostare questo comando per eliminare automaticamente il contenitore all'arresto del contenitore. Ad esempio, usa docker run -it --rm per eseguire due operazioni: innanzitutto, usa automaticamente il terminale corrente per connettersi al contenitore e poi, quando il contenitore si chiude, rimuovilo.

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 contare solo fino a tre, passare 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 il processo viene arrestato. Assicurati che non esista.

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

Modificare ENTRYPOINT

Il comando consente anche di modificare il comando dal Dockerfile ed eseguire qualcos'altro, ma solo per tale contenitore. Ad esempio, usare il comando seguente per eseguire bash o cmd.exe. Modificare il comando in base alle esigenze.

In questo esempio ENTRYPOINT viene modificato in cmd.exe. Viene premuto CTRL+C per terminare il processo e fermare 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

Nota

Questo esempio funziona solo nei contenitori di Windows. I contenitori Linux non hanno cmd.exe.

Comandi essenziali

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

Pulire le risorse

Durante questa esercitazione sono stati creati contenitori e immagini. Se vuoi, elimina queste risorse. Usare i comandi seguenti per

  1. Elencare tutti i contenitori

    docker ps -a
    
  2. Fermare i contenitori in esecuzione per nome.

    docker stop core-counter
    
  3. Eliminare il contenitore

    docker rm core-counter
    

Eliminare quindi tutte le immagini che non si desiderano più nel computer. Eliminare l'immagine creata dal Dockerfile e quindi eliminare l'immagine .NET su cui si basava il Dockerfile . È possibile usare l'ID IMMAGINE o la stringa formattata REPOSITORY:TAG.

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

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

Suggerimento

I file di immagine possono essere di grandi dimensioni. In genere, si rimuoverebbero i contenitori temporanei creati durante il test e lo sviluppo dell'app. In genere si mantengono le immagini di base con il runtime installato se si prevede di creare altre immagini in base a tale runtime.

Passaggi successivi