SQL Server + Docker Compose: como criar e popular bancos de dados ao inicializar os containers
Tags: csharp, sql, docker, braziliandevs
Published: 2024-06-24 13:47:55


Fazer uso de **containers** é uma excelente abordagem para automatizar o funcionamento e a distribuição de projetos. Entretanto, se você está criando uma aplicação **ASP.NET Core** com **Entity Framework Core**, você provavelmente utilizou **migrations** para construir o banco de dados.

Agora, como poderíamos automatizar a inicialização dessa aplicação? Através de do **Startup** do **SQL Server**.

O SQL Server possui uma série de aplicativos funcionais em seu core. Um deles se chama `sqlcmd`. Esse executável permite com que você possa realizar comandos instantâneos no SQL Server sem necessariamente conectar a sua aplicação ou um gerenciador de banco de dados e está disponível para uso na imagem Docker do SQL Server.

:warning: **Atenção**: Se você está utilizando um computador com processador ARM, provavelmente você não utilizou a imagem do SQL Server mas sim a imagem do Azure SQL Edge. Lembre-se que a imagem do Azure SQL não possui o executável `sqlcmd` e por isso, não funcionará.

Para mostrar a automatização do processo, utilizei uma API simples utilizando .NET 6 e EF Core chamada ToDo.API.


## 1º Passo

Iremos criar um arquivo docker-compose.yml com a imagem do SQL Server e garantir que exista um volume nela. Este volume foi feito com o bind `/data` -> `/initdb` e servirá para colocarmos um script SQL capaz de construir o banco de dados.

```yml
version: '3'
services:
  db:
    image: mcr.microsoft.com/mssql/server:2019-latest
    volumes:
      - ./data:/initdb
    container_name: todo-database
    ports:
        - 1433:1433
    environment:
      - ACCEPT_EULA=Y
      - MSSQL_SA_PASSWORD=TodoApiSqlPass123!
      - MSSQL_PID=Developer
```

## 2º Passo

Agora, iremos criar as migrations para a nossa aplicação. Será muito importante garantir que exista apenas uma única migrations capaz de construir o banco de dados inteiro. Podemos fazer isso com o seguinte comando dentro do diretório da API:

```shell
dotnet ef migrations add initialcreate
```

## 3º Passo

O próximo passo será criar um script com o código dessa migration. Como ela é a única, não precisamos nos preocupar em indicar qual migration criar o script. Iremos salvar esse script dentro do diretório `/data`, para onde apontamos o volume:

```shell
 dotnet ef migrations script -o ../data/initial.sql
```

## 4º Passo

Agora iremos editar o arquivo `/data/initial.sql` para indicar o banco que estamos trabalhando. Nesta API, configurei a API para se conectar a um banco de dados chamado `Todo`. Portanto, no início do arquivo `initial.sql`, iremos colocar as seguintes instruções:


```sql
CREATE DATABASE Todo;
GO

USE Todo;
GO
```


No final desse arquivo, você pode criar instruções de `INSERT` para popular o seu banco de dados como um `seeder`.

## 5º Passo

Podemos então criar um `Dockerfile` para criar uma imagem da API. Dentro do diretório da API, criaremos o seguinte:

```dockerfile
FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build-env
WORKDIR /app

COPY . ./
RUN dotnet restore
RUN dotnet publish -c Release -o out

FROM mcr.microsoft.com/dotnet/aspnet:6.0
WORKDIR /app
COPY --from=build-env /app/out .

ENTRYPOINT ["dotnet", "ToDo.Api.dll"]
```

Esse é o modelo de Dockerfile mais simples para aplicações `ASP.NET` e você pode construir o mesmo da maneira desejada.


## 6º Passo

Agora podemos finalmente adicionar o serviço da API dentro do `docker-compose.yml` como um serviço dependente do banco de dados:

```yml
api:
    container_name: todo-api
    build: ./ToDo.Api
    ports:
      - 5501:5501
    depends_on:
      - db
```

## 7º Passo

Nosso último passo e mais importante. Popular o banco de dados. Para isso, iremos criar um shell script. Mas vamos entender como funciona o processo.

Dentro do container do banco de dados, temos o executável `sqlcmd` como mencionado anteriormente, localizado em `/opt/mssql-tools/bin/sqlcmd`.

Este executável pode ser chamado com os seguintes parâmetros:

```shell
sqlcmd -S server -U usuario -P senha -d banco -i script.sql
```

Onde:
-S: corresponde ao servidor de conexão com o SQL Server;
-U: corresponde ao usuário;
-P: corresponde à senha;
-d: corresponde ao banco onde será feita a conexão;
-i: corresponde ao script SQL que será executado.

Para a nossa aplicação, podemos chamar então o `sqlcmd` com um `docker exec` da seguinte maneira:

```shell
 docker exec todo-database /opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P TodoApiSqlPass123! -d master -i initdb/initial.sql
```

Nesse comando, executamos dentro do container `todo-database`, nomeado no arquivo `docker-compose.yml` passando o server, usuário e senha também configurados no mesmo. Para o banco de dados, selecionamos `master`, pois é o banco de maior acesso em um server novo, e ainda não possuímos o nosso banco.

Quanto ao script, passamos o diretório `/initdb` que criamos no volume do container junto do arquivo `initial.sql` criado pela migration.

## 8º Passo

Entretanto, para deixar algo mais automatizado, iremos criar um script shell para executar tal comando.  Portanto, iremos criar um arquivo chamado `init.sh` na raiz do repositório com o seguinte conteúdo:

```sh
#!/bin/bash
while :
do 
  PORTS=`lsof -i:1433`
  echo $PORTS
  if ["$PORTS" = '']; then
    echo 'Database is down, trying to reconnect to run initial migrations!'
    sleep 5s
  else
    docker exec todo-database /opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P TodoApiSqlPass123! -d master -i initdb/initial.sql; echo "All done!";
    break
  fi
done

echo "Successfuly executed!"
```

:warning: **Atenção**: Este script está preparado para computadores Linux ou Mac mas você pode adaptar para Windows com o Powershell.

Como funciona esse script?

Primeiramente, como o container de um banco de dados SQL Server pode ser demorado a subir, temos que garantir algo para validar se o mesmo está operacional. Esse container está expondo a porta 1433 e com ela, iremos verificar se o banco está operacional. 

O script roda o comando `lsof -i:1433` para verificar os processo executando nessa porta. Se não encontrar nenhum processo usando a porta, o mesmo irá imprimir a mensagem `Database is down, trying to reconnect to run initial migrations!` e irá esperar por 05 segundos para tentar novamente.

Caso o mesmo encontre o container do banco de dados, irá executar o `sqlcmd` e ao fim do processo, imprimir as mensagens `All done!` e `Successfuly executed!`.

Para facilitar a visualização, você pode clonar o [repositório completo](https://github.com/Danilo-Oliveira-Silva/SqlServer-Startup).

<h3 style="color:#000066">Danilo Silva</h3>
<img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tj4i67b3yid2wlx90oky.png" />

Desenvolvedor de software experiente em boas práticas, clean code e no desenvolvimento de software embarcado e de integração com hardwares de controle e telecomunicação.

<a href="https://www.linkedin.com/in/danilo-silva-44518956/">Linkedin</a>
<a href="https://github.com/Danilo-Oliveira-Silva">Github</a>
<a href="https://twitter.com/danilosdev">Twitter</a>
<a href="mailto:danilo.o.s@hotmail.com">E-mail</a>