Docker - Usando Docker-Compose, ASP .NET Core e SQL Server com VS Code


Hoje veremos como criar um arquivo Docker-Compose para uma aplicação ASP .NET Core que usa um banco de dados SQL Server usando o EF Core na abordagem Code-First.

Se você esta chegando agora e não sabe o que é Docker sugiro que acompanhe o meu curso de introdução ao Docker nesta série de artigos: Docker - Uma introdução básica (Veja também o meu curso de Docker na Udemy)

Objetivos e requisitos

Vamos criar uma aplicação ASP .NET Core MVC para gerenciar informações sobre Livros como : Id e Titulo, fazendo um CRUD básico, e vamos conteinerizar esta aplicação MVC.

Nesta aplicação vamos usar o EF Core na abordagem Code-First e vamos usar o SQL Server em um contêiner. Assim teremos dois contêineres : a aplicação MVC  que vai acessar o contêiner do banco de dados SQL Server.

Os recursos que iremos usar são todos grátis:

A primeira a coisa a fazer é instalar ao .NET Core SDK no seu ambiente, e a seguir os demais recursos. Após a instalação, para verificar o ambiente abra uma janela do PowerShell e digite o comando:  dotnet --version

A seguir temos que instalar a ferramenta  dotnet-ef do EF Core no ambiente do .NET Core. Para isso digite o seguinte comando :
 dotnet tool install --global dotnet ef

A seguir para verificar digite o comando : dotnet ef

Criando a aplicação ASP .NET Core MVC

Para criar a aplicação ASP .NET Core MVC crie uma pasta onde deseja criar o projeto e a partir desta pasta digite o comando em uma janela de prompt de comandos:

dotnet new mvc -n "livraria" -lang "C#" -au none

Será criada a pasta livraria e nesta pasta teremos o projeto MVC criando.

Estando dentro da pasta do projeto vamos instalar os pacotes do EF Core digitando os comandos:

dotnet add package Microsoft.EntityFrameworkCore --version 5.0.5
dotnet add package Microsoft.EntityFrameworkCore.SqlServer --version 5.0.5
dotnet add package Microsoft.EntityFrameworkCore.Design --version 5.0.5

Após esta instalação podemos verificar os pacotes incluídos no arquivo livraria.csproj :

Vamos buildar o projeto e executá-lo para ter certeza de que não existem erros:

dotnet build



dotnet run

Vamos criar agora o modelo de domínio da aplicação. Na pasta Models do projeto criado inclua a classe Livro:

public class Livro
{
   public int Id { get; set; }
   public string Titulo { get; set; }
}

A seguir nesta mesma pasta crie o arquivo de contexto ApplicationDbContext que herda da classe DbContext do EF Core:

using Microsoft.EntityFrameworkCore;
namespace livraria.Models
{
    public class ApplicationDbContext : DbContext
    {
        public DbSet<Livro> Livros { get; set; }
 
       public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options)
        { }
    }
}

Vamos agora registrar o serviço do contexto no arquivo Startup:

 public void ConfigureServices(IServiceCollection services)
 {
            var server = Configuration["DbServer"] ?? "localhost";
            var port = Configuration["DbPort"] ?? "1433"; // Default SQL Server port
            var user = Configuration["DbUser"] ?? "SA"; // Warning do not use the SA account
            var password = Configuration["Password"] ?? "numsey#2021";
            var database = Configuration["Database"] ?? "LivrosDb";
            var connectionString = $"Server={server}, {port};Initial Catalog={database};User ID={user};Password={password}";
            services.AddDbContext<ApplicationDbContext>(options => 
                options.UseSqlServer(connectionString));
             services.AddControllersWithViews();
  }

Aqui além de registrar o serviço do contexto criamos a string de conexão definindo valores para as variáveis de ambiente para definir o nome do servidor, a porta , o usuário, a senha e o nome do banco de dados.

Vamos gerar o script de migração para o SQL Server digitando o comando:

dotnet ef database migrations add "Migracao_Inicial"

Não vamos executar o comando update-database pois não vamos aplicar o Migrations diretamente no SQL Server  pois ainda não temos o banco de dados pois ele será inicializado em um contêiner Docker.

Vamos criar um serviço de migração de banco de dados, para executar a migração no contêiner quando da inicialização da nossa aplicação.

Para isso vamos criar uma pasta chamada Services, e dentro dessa pasta vamos criar a classe DatabaseManagementService :

using livraria.Models;
using Microsoft.AspNetCore.Builder;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
namespace livraria.Services
{
    public static class DatabaseManagementService
    {
         
        public static void MigrationInitialisation(IApplicationBuilder app)
        {
            using (var serviceScope = app.ApplicationServices.CreateScope())
            {
                serviceScope.ServiceProvider.GetService<ApplicationDbContext>().Database.Migrate();
            }
        }
    }
}

Este método vai aplicar a migração inicial assim que aplicação for iniciada pela primeira vez.

A seguir vamos incluir a chamada deste método no método Configure da classe Startup :

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{

   DatabaseManagementService.MigrationInitialisation(app);
   ...
}

Criando o Controller e as Views

Vamos criar o controlador LivrariaController na pasta Controllers e incluir dois métodos Action:

  1. Um para exibir uma lista de livros  - Index
  2. Outro para criar um novo livro - Create
using System;
using System.Linq;
using System.Threading.Tasks;
using livraria.Models;
using Microsoft.AspNetCore.Mvc;
namespace livraria.Controllers
{
    public class LivrariaController : Controller
    {
        private readonly ApplicationDbContext _context;
        public LivrariaController(ApplicationDbContext context)
        {
            _context = context;
        }
        public IActionResult Index()
        {
            var books = _context.Livros.ToList();
            return View(books);
        }
        [HttpGet]
        public IActionResult Create()
        {
            return View();
        }
        [HttpPost]
        public async Task<IActionResult> Create(Livro livro)
        {
            if (ModelState.IsValid)
            {
                try
                {
                    _context.Livros.Add(livro);
                    await _context.SaveChangesAsync();
                    return RedirectToAction("Index");
                }
                catch(Exception ex)
                {
                    ModelState.AddModelError(string.Empty, $"Algo deu errado {ex.Message}");
                }
            }
            ModelState.AddModelError(string.Empty, $"Algo deu errado, modelo inválido");
            return View(livro);
        }        
    }
}

Vamos agora criar as Views Index e Create na pasta /Views/Livraria do projeto:

1- Index.cshtml

@model IEnumerable<livraria.Models.Livro>

<h2>Livraria</h2>
<hr />
<div class="row">
    <div class="col-md-12">
        <div>
            <div class="pull-left">
                <a asp-action="Create" class="btn btn-primary">
                    <span title="Incluir Livro" class="fas fa-plus"></span> Incluir Livro
                </a>
            </div>
        </div>
    </div>
</div>
<br />
<div class="panel panel-default">
    <table class="table table-striped">
        <thead>
            <tr>
                <th>
                    @Html.DisplayNameFor(model => model.Titulo)
                </th>
                <th></th>
            </tr>
        </thead>
        <tbody>
            @foreach (var item in Model)
            {
                <tr>
                    <td>
                        @Html.DisplayFor(modelItem => item.Titulo)
                    </td>
                    <td>
                        <a asp-action="Edit" asp-route-id="@item.Id" title="Edita" class="btn btn-warning">
                            <span class="fas fa-edit">Edita</span>
                        </a>

                        <a asp-action="Delete" asp-route-id="@item.Id" title="Deleta" class="btn btn-danger">
                            <span class="fas fa-trash-alt">Deleta</span>
                        </a>
                    </td>
                </tr>
            }
        </tbody>
    </table>
</div>

2- Create.cshtml

@model livraria.Models.Livro
<h2>Novo Livro</h2>
<form asp-action="Create" method="post">
    <div class="form-horizontal">
        <hr />
        <div class="form-group">
            <label asp-for="Titulo" class="col-md-2 control-label"></label>
            <div class="col-md-10">
                <input asp-for="Titulo" class="form-control" />
                <span asp-validation-for="Titulo" class="text-danger"></span>
            </div>
        </div>
        <div class="form-group">
            <div class="col-md-offset-2 col-md-10">
                <input type="submit" value="Create" class="btn btn-success" />
                <a asp-action="Index" class="btn btn-info">Retornar</a>
            </div>
        </div>
    </div>
</form>

Pronto. Nossa aplicação ASP .NET Core MVC já esta pronta para exibir e incluir livros. Vamos fazer um último ajuste no arquivo Program do projeto incluindo o código abaixo para forçar a aplicação usar a porta 80.

...
public static IHostBuilder CreateHostBuilder(string[] args) =>
       Host.CreateDefaultBuilder(args)
               .ConfigureWebHostDefaults(webBuilder =>
                {
                    webBuilder.UseUrls("http://*:80");
                    webBuilder.UseStartup<Startup>();
            });
...

Vamos agora criar um arquivo Dockerfile no projeto com o código abaixo:

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

# Copy the csproj and restore all of the nugets
COPY *.csproj ./
RUN dotnet restore

# Copy everything else and build
COPY . ./
RUN dotnet publish -c Release -o out

# Build runtime image
FROM mcr.microsoft.com/dotnet/sdk:5.0
WORKDIR /app
EXPOSE 80
COPY --from=build-env /app/out .
ENTRYPOINT ["dotnet", "Livraria.dll"]

Código no VS Code :

Criando a imagem e contêiner para a aplicação ASP .NET Core

Já temos tudo pronto vamos criar a imagem para a nossa aplicação ASP .NET Core e a seguir criar e executar o respectivo contêiner.

Para criar a imagem digite o comando :  docker build -t livraria .

Ao final podemos verificar a imagem criada usando o comando : docker images

Para criar e executar um contêiner com essa imagem vamos usar o comando:

docker run -p 8080:80 livraria

Mas o contêiner vai falhar na execução pois ainda não temos o banco de dados SQL Server pronto. Vamos fazer isso agora.

Vamos usar uma imagem do SQL Server a partir do repositório oficial do SQL Server no Docker Hub.

O nome da imagem : mcr.microsoft.com/mssql/server

A tag  : 2017-latest-ubuntu

Assim para baixar esta imagem podemos usar o comando: 
docker pull mcr.microsoft.com/mssql/server:2017-latest-ubuntu

Poderíamos tentar criar e executar um contêiner com essa imagem usando o seguinte comando:

docker run -d mcr.microsoft.com/mssql/server:2017-latest-ubuntu -e 'ACCEPT_EULA=Y' -e 'SA_PASSWORD=numsey#2021' -e 'MSSQL_PID=Express' -p 1433:1433

Aqui estamos definindo as variáveis de ambiente:

E o mapeamento da porta '1433:1433'.

E a seguir levantar o contêiner da aplicação ASP .NET Core e tentar conectar os dois contêineres mas isso iria falhar pois os contêineres não estão na mesma redes.

Orquestrando os contêineres com Docker-Compose

Para podermos acessar o contêiner do  SQL Server a partir do contêiner da aplicação ASP .NET Core MVC vamos criar um arquivo Docker-Compose para orquestrar os dois contêineres.

Assim crie na pasta do projeto o arquivo docker-compose.yml com o seguinte código:

version: '3'
services:
  mssql-server:
    image: mcr.microsoft.com/mssql/server:2017-latest-ubuntu
    environment:
      ACCEPT_EULA: "Y"
      SA_PASSWORD: "numsey#2021"
      MSSQL_PID: Express
    ports:
      - "1433:1433"
    volumes:
      - C:\dados\volumes\sqlserver:/var/opt/mssql/data
  livro-app:
    build: .
    environment:
      DbServer: "mssql-server"
      DbPort: "1433"
      DbUser: "SA"
      Password: "numsey#2021"
      Database: "LivrosDb"
    ports: 
      - "8090:80"

Código no VS Code :

Vamos entender o arquivo docker-compose.yml acima:

1- Criamos o serviço para o SQL Server definindo a imagem e os valores das variáveis de ambiente que serão usadas para preencher os dados da string de conexão e definimos a porta como 1433.

Aqui criamos um volume de forma a que quando o contêiner for destruído os dados estarão persistidos localmente na pasta indicada.

2- Criamos o serviço para a aplicação ASP .NET Core onde estamos dando um build no contexto do Dockerfile e definimos as variáveis de ambientes que para fazer a conexão com o SQL Server definindo o usuário, a senha e o banco de dados e definimos o mapeamento da porta de forma que vamos acessar a porta 8090.

Agora é só alegria....

Basta executar o comando :  docker-compose up --build

E teremos os dois contêineres em execução:

Acessando o endereço http://localhost:8090 veremos nossa aplicação MCV e assim poderemos incluir livros no SQL Server e exibir os livros registrados conforme mostra a figura abaixo:

Vimos assim como criar um contêiner para uma aplicação ASP .NET Core e um contêiner para uma banco de dados SQL Server e fazer a comunicação entre os dois contêineres.

Se você der uma espiada na pasta c:\dados\volumes\sqlserver vai verificar a cópia do banco de dados usados pela aplicação ASP .NET Core MVC , graças ao volume que criamos. Assim o contêiner poderá ser destruído e os dados estarão preservados.

Pegue o projeto aqui : Livraria.zip (sem as referências)

"E em nada vos espanteis dos que resistem, o que para eles, na verdade, é indício de perdição, mas para vós de salvação, e isto de Deus. Porque a vós vos foi concedido, em relação a Cristo, não somente crer nele, como também padecer por ele"
Filipenses 1:28,29

Referências:


José Carlos Macoratti