Tutorial: Inclusión de una aplicación de .NET en un contenedor

En este tutorial obtendrá información sobre cómo incluir una aplicación de .NET en un contenedor con Docker. Los contenedores tienen muchas características y ventajas, como ser una infraestructura inmutable, proporcionar una arquitectura portátil y permitir la escalabilidad. La imagen puede usarse para crear contenedores para un entorno de desarrollo local, una nube privada o una nube pública.

En este tutorial ha:

  • Crear y publicar una aplicación .NET sencilla
  • Crear y configurar un archivo Dockerfile para .NET
  • Creación de una imagen de Docker
  • Crear y ejecutar un contenedor de Docker

Aprenderá sobre las tareas de compilación e implementación de un contenedor de Docker para una aplicación .NET. La plataforma Docker usa el motor de Docker para compilar y empaquetar rápidamente aplicaciones como imágenes de Docker. Estas imágenes se escriben en el formato Dockerfile para implementarse y ejecutarse en un contenedor superpuesto.

Nota

Este tutorial no es para aplicaciones ASP.NET Core. Si usa ASP.NET Core, lea el tutorial sobre cómo incluir una aplicación de ASP.NET Core en un contenedor.

Requisitos previos

Instale estos requisitos previos:

  • SDK de .NET 8 y versiones posteriores
    Si tiene instalado .NET, use el comando dotnet --info para determinar el SDK que está usando.
  • Docker Community Edition
  • Una carpeta de trabajo temporal para Dockerfile y una aplicación .NET de ejemplo. En este tutorial, el nombre docker-working se usa como la carpeta de trabajo.
  • SDK de .NET 7 (y posteriores)
    Si tiene instalado .NET, use el comando dotnet --info para determinar el SDK que está usando.
  • Docker Community Edition
  • Una carpeta de trabajo temporal para Dockerfile y una aplicación .NET de ejemplo. En este tutorial, el nombre docker-working se usa como la carpeta de trabajo.

Creación de aplicaciones .NET

Necesita una aplicación .NET que el contenedor Docker ejecuta. Abra el terminal, cree una carpeta de trabajo si todavía no lo ha hecho y entre en ella. En la carpeta de trabajo, ejecute el comando siguiente para crear un proyecto en un subdirectorio llamado App:

dotnet new console -o App -n DotNet.Docker

El árbol de carpetas es similar al siguiente:

📁 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

Con el comando dotnet new se crea una carpeta denominada App y se genera una aplicación de consola "Hola mundo". Cambie los directorios y vaya a la carpeta App, desde la sesión de Terminal. Use el comando dotnet run para iniciar la aplicación. La aplicación se ejecuta e imprime Hello World! debajo del comando:

cd App
dotnet run
Hello World!

La plantilla predeterminada crea una aplicación que imprime en el terminal y termina de inmediato. En este tutorial, se usa una aplicación que se repite en bucle de manera indefinida. Abra el archivo Program.cs en un editor de texto.

Sugerencia

Si está utilizando Visual Studio Code, en la sesión de Terminal anterior, escriba el siguiente comando:

code .

Se abrirá la carpeta App que contiene el proyecto en Visual Studio Code.

El archivo Program.cs debería ser similar al código de C# siguiente:

Console.WriteLine("Hello World!");

Reemplace el archivo por el código siguiente que cuenta números 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));
}

Guarde el archivo y vuelva a probar el programa con dotnet run. Recuerde que esta aplicación se ejecuta de manera indefinida. Use el comando Cancelar Ctrl+C para detenerla. A continuación se muestra un resultado de ejemplo:

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

Si pasa un número en la línea de comandos a la aplicación, solo se contará hasta esa cantidad y se cerrará. Inténtelo con dotnet run -- 5 para contar hasta cinco.

Importante

Cualquier parámetro posterior a -- no se pasa al comando dotnet run y, en su lugar, se pasa a su aplicación.

Publicación de una aplicación .NET

Antes de agregar la aplicación .NET a la imagen de Docker, primero debe publicarse. Es mejor que el contenedor ejecute la versión publicada de la aplicación. Ejecute el comando siguiente para publicar la aplicación:

dotnet publish -c Release

Con este comando se compila la aplicación en la carpeta publish. La ruta de acceso a la carpeta publish desde la carpeta de trabajo debería ser .\App\bin\Release\net8.0\publish\.

Con este comando se compila la aplicación en la carpeta publish. La ruta de acceso a la carpeta publish desde la carpeta de trabajo debería ser .\App\bin\Release\net7.0\publish\.

En la carpeta App, obtenga un listado de los directorios de la carpeta publish para comprobar que se ha creado el archivo DoNet.Docker.dll.

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

Creación del archivo Dockerfile

El archivo Dockerfile lo usa el comando docker build para crear una imagen de contenedor. Este archivo es un archivo de texto denominado Dockerfile que no tiene ninguna extensión.

Cree un archivo denominado Dockerfile en el directorio que contiene el archivo .csproj y ábralo en un editor de texto. En este tutorial se usa la imagen del runtime de ASP.NET Core (que contiene la imagen de runtime de .NET) y corresponde a la aplicación de consola de .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:

Aquí se usa intencionadamente la imagen de runtime de ASP.NET Core, aunque se podría haber usado la imagen de mcr.microsoft.com/dotnet/runtime:8.0.

Sugerencia

Este Dockerfile usa compilaciones de varias fases, lo que optimiza el tamaño final de la imagen mediante la disposición en capas de la compilación y dejando solo los artefactos necesarios. Para obtener más información, vea Documentación de Docker: compilaciones de varias fases.

La palabra clave FROM requiere un nombre completo de imagen de contenedor de Docker. Microsoft Container Registry (MCR, mcr.microsoft.com) es un sindicato de Docker Hub, que hospeda contenedores a los que se puede acceder de forma pública. El segmento dotnet es el repositorio de contenedores, mientras que los segmentos sdk o aspnet son el nombre de la imagen de contenedor. La imagen está etiquetada con 8.0, que se usa para el control de versiones. Por tanto, mcr.microsoft.com/dotnet/aspnet:8.0 es el runtime de .NET 8.0. Asegúrese de extraer el runtime que coincida con el que el SDK tiene como destino. Por ejemplo, la aplicación creada en la sección anterior usaba el SDK de .NET 8.0, y la imagen base a la que se hace referencia en Dockerfile se etiqueta con 8.0.

Importante

Al usar imágenes de contenedor basadas en Windows, debe especificar la etiqueta de imagen más allá de simplemente 8.0, por ejemplo, mcr.microsoft.com/dotnet/aspnet:8.0-nanoserver-1809 en lugar de mcr.microsoft.com/dotnet/aspnet:8.0. Seleccione un nombre de imagen en función de si usa Nano Server o Windows Server Core y qué versión de ese sistema operativo usa. Puede encontrar una lista completa de todas las etiquetas compatibles en la página de Docker Hub de .NET.

Guarde el archivo Dockerfile. La estructura de directorios de la carpeta de trabajo debería tener el siguiente aspecto. Algunos de los archivos y carpetas inferiores se han omitido para ahorrar espacio en este artículo:

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

Aquí se usa intencionadamente la imagen de runtime de ASP.NET Core, aunque se podría haber usado la imagen de mcr.microsoft.com/dotnet/runtime:7.0.

Sugerencia

Este Dockerfile usa compilaciones de varias fases, lo que optimiza el tamaño final de la imagen mediante la disposición en capas de la compilación y dejando solo los artefactos necesarios. Para obtener más información, vea Documentación de Docker: compilaciones de varias fases.

La palabra clave FROM requiere un nombre completo de imagen de contenedor de Docker. Microsoft Container Registry (MCR, mcr.microsoft.com) es un sindicato de Docker Hub, que hospeda contenedores accesibles públicamente. El segmento dotnet es el repositorio de contenedores, mientras que los segmentos sdk o aspnet son el nombre de la imagen de contenedor. La imagen está etiquetada con 7.0, que se usa para el control de versiones. Por tanto, mcr.microsoft.com/dotnet/aspnet:7.0 es el entorno de ejecución de .NET 7.0. Asegúrese de extraer el runtime que coincida con el que el SDK tiene como destino. Por ejemplo, la aplicación creada en la sección anterior usaba el SDK de .NET 7.0, y la imagen base a la que se hace referencia en el documento Dockerfile se etiqueta con 7.0.

Guarde el archivo Dockerfile. La estructura de directorios de la carpeta de trabajo debería tener el siguiente aspecto. Algunos de los archivos y carpetas inferiores se han omitido para ahorrar espacio en este artículo:

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

Desde un terminal, ejecute el comando siguiente:

docker build -t counter-image -f Dockerfile .

Docker procesará cada línea en el archivo Dockerfile. En . el comando docker build establece el contexto de compilación de la imagen. El modificador -f es la ruta de acceso al archivo Dockerfile. Este comando crea la imagen y un repositorio local denominado counter-image que apunta a esa imagen. Una vez que finalice este comando, ejecute docker images para ver una lista de las imágenes instaladas:

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

El repositorio counter-image es el nombre de la imagen. La etiqueta latest es la etiqueta que se usa para identificar la imagen. 2f15637dc1f6 es el id. de la imagen. 10 minutes ago especifica la hora de creación de la imagen. 217MB es el tamaño de la imagen. Los pasos finales del archivo Dockerfile son crear un contenedor a partir de la imagen y ejecutar la aplicación, copiar la aplicación publicada en el contenedor y definir el punto 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

El repositorio counter-image es el nombre de la imagen. La etiqueta latest es la etiqueta que se usa para identificar la imagen. 2f15637dc1f6 es el id. de la imagen. 10 minutes ago especifica la hora de creación de la imagen. 208MB es el tamaño de la imagen. Los pasos finales del archivo Dockerfile son crear un contenedor a partir de la imagen y ejecutar la aplicación, copiar la aplicación publicada en el contenedor y definir el punto de entrada.

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

El comando COPY indica a Docker que copie la carpeta especificada en el equipo a una carpeta del contenedor. En este ejemplo, la carpeta publish se copia en una carpeta denominada App/out del contenedor.

El comando WORKDIR cambia el directorio actual dentro del contenedor a App.

El comando siguiente, ENTRYPOINT, indica a Docker que configure el contenedor para que se ejecute como ejecutable. Cuando el contenedor se inicia, se ejecuta el comando ENTRYPOINT. Cuando este comando finaliza, el contenedor se detiene automáticamente.

Sugerencia

Antes de .NET 8, los contenedores configurados para ejecutarse como de solo lectura pueden producir un error con Failed to create CoreCLR, HRESULT: 0x8007000E. Para solucionar este problema, especifique una variable de entorno DOTNET_EnableDiagnostics como 0 (justo antes del paso ENTRYPOINT):

ENV DOTNET_EnableDiagnostics=0

Para obtener más información sobre las diversas variables de entorno de .NET, consulte Variables de entorno de .NET.

Nota:

.NET 6 estandariza en el prefijo DOTNET_ en lugar de en COMPlus_ para las variables de entorno que configuran el comportamiento en tiempo de ejecución de .NET. Sin embargo, el prefijo COMPlus_ seguirá funcionando. Si usa una versión anterior del runtime de .NET, debe seguir usando el prefijo COMPlus_ para las variables de entorno.

Crear un contenedor

Ahora que tiene una imagen que contiene la aplicación, puede crear un contenedor. Hay dos formas de crear un contenedor. En primer lugar, cree un contenedor que esté detenido.

docker create --name core-counter counter-image

El comando docker create crea un contenedor basado en la imagen counter-image. La salida de ese comando muestra el valor CONTAINER ID (el suyo será distinto) del contenedor creado:

d0be06126f7db6dd1cee369d911262a353c9b7fb4829a0c11b4b2eb7b2d429cf

Para ver una lista de todos los contenedores, use el 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

Administración del contenedor

El contenedor se creó con un nombre específico, core-counter, usado para administrar el contenedor. En el ejemplo siguiente se usa el comando docker start para iniciar el contenedor y luego se usa el comando docker ps para mostrar solo los contenedores que están en ejecución:

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

Del mismo modo, el comando docker stop detiene el contenedor. En el ejemplo siguiente se usa el comando docker stop para detener el contenedor y luego se usa el comando docker ps para mostrar que no hay ningún contenedor en ejecución:

docker stop core-counter
core-counter

docker ps
CONTAINER ID    IMAGE    COMMAND    CREATED    STATUS    PORTS    NAMES

Conectarse a un contenedor

Una vez que se ejecuta un contenedor, puede conectarse a él para ver la salida. Use los comandos docker start y docker attach para iniciar el contenedor y echar un vistazo al flujo de salida. En este ejemplo, la pulsación de teclas Ctrl+C se usa para desasociarse del contenedor en ejecución. A menos que se especifique lo contrario, al pulsar esta tecla finaliza el proceso en el contenedor, con lo que se detendría el contenedor. El parámetro --sig-proxy=false garantiza que Ctrl+C no va a detener el proceso en el contenedor.

Después de desasociarse del contenedor, reasócielo para comprobar que sigue en ejecución.

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

Eliminación de un contenedor

En este artículo, no quiere que haya contenedores que no hagan nada. Elimine el contenedor que creó anteriormente. Si el contenedor está en ejecución, deténgalo.

docker stop core-counter

En el ejemplo siguiente se muestran todos los contenedores. Luego, se usa el comando docker rm para eliminar el contenedor y después se vuelve a comprobar si hay algún contenedor en ejecución.

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

Ejecución única

Docker proporciona el comando docker run para crear y ejecutar el contenedor como comando único. Este comando elimina la necesidad de ejecutar docker create y luego docker start. También puede establecer este comando en que elimine automáticamente el contenedor cuando este se detenga. Por ejemplo, use docker run -it --rm para hacer dos cosas: primero, use automáticamente el terminal actual para conectarse al contenedor y cuando el contenedor finalice, quítelo:

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

El contenedor también pasa parámetros a la ejecución de la aplicación .NET. Para indicar a la aplicación .NET que cuente solo hasta 3, pase 3.

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

Con docker run -it, el comando Ctrl+C detiene el proceso que se ejecuta en el contenedor, lo que, a su vez, detiene el contenedor. Como se proporcionó el parámetro --rm, el contenedor se elimina automáticamente cuando se detiene el proceso. Compruebe que no existe:

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

Cambio de ENTRYPOINT

El comando docker run también permite modificar el comando ENTRYPOINT desde el archivo Dockerfile y ejecute algún otro elemento, pero solo para ese contenedor. Por ejemplo, use el comando siguiente para ejecutar bash o cmd.exe. Edite el comando según sea necesario.

En este ejemplo, ENTRYPOINT cambia a cmd.exe. Se presiona Ctrl+C para finalizar el proceso y detener el contenedor.

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 esenciales

Docker tiene muchos comandos diferentes que crean, administran e interactúan con contenedores e imágenes. Estos comandos de Docker son esenciales para la administración de los contenedores:

Limpiar los recursos

Durante este tutorial, creó contenedores e imágenes. Elimine estos recursos si quiere hacerlo. Use los comandos siguientes para

  1. Mostrar todos los contenedores

    docker ps -a
    
  2. Detener los contenedores que están en ejecución por nombre

    docker stop core-counter
    
  3. Eliminar el contenedor

    docker rm core-counter
    

A continuación, elimine las imágenes que ya no quiere tener en la máquina. Elimine la imagen que creó el archivo Dockerfile y luego elimine la imagen de .NET en que se basó el archivo Dockerfile. Puede usar el valor IMAGE ID o la cadena con formato 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

Use el comando docker images para ver una lista de las imágenes instaladas.

Sugerencia

Los archivos de imagen pueden ser grandes. Por lo general, quitaría los contenedores temporales que creó al probar y desarrollar la aplicación. Habitualmente, estas imágenes base se conservan con el runtime instalado si se planea crear otras imágenes basadas en ese runtime.

Pasos siguientes