ASP.NET Core + Docker Compose: implementando soluções Web multi-containers
O autor mostra neste artigo como implementar soluções Web multi-containers com o ASP.NET Core e o Docker Compose.

Contando com suporte às principais plataformas de desenvolvimento da atualidade, containers Docker↳Docker46 conteúdosE o Docker Swarm? Contextos e motivadores diáriosDevSecOps · ago 2024Automatizando o ambiente de desenvolvimento e testes com DockerDevSecOps · mai 2019MySQL + Adminer + Docker Compose: montando rapidamente um ambiente para usoData · abr 2019Ver tudo em DevSecOps → vêm se tornando uma presença frequente em projetos com os mais variados escopos. Dentre as vantagens oferecidas por esta tecnologia, estão o isolamento entre aplicações, uma utilização mais racional de recursos computacionais, além de maior agilidade em processos de deployment.
A geração de um novo container para a execução de uma aplicação depende de uma imagem criada previamente. Trata-se de um processo extremamente simples e que já abordei envolvendo diferentes cenários nos seguintes artigos:
- Descomplicando a montagem de ambientes de Testes com Docker – QAXperience 2018
- ASP.NET Core + Docker: aplicações na nuvem com Azure Container Instances
- ASP.NET Core 2.0: CRUD em API utilizando JWT, EF Core InMemory e Docker
- ASP.NET Core 2.0: deployment na nuvem com Docker, Azure Container Registry e Azure Web App on Linux
- ASP.NET Core 2.0: publicando no Docker Hub a partir do VS 2017
- .NET + SQL Server 2017 – Parte 1: executando o SQL Server em um container Docker
- Docker + NoSQL: executando o MongoDB e o Redis a partir de containers
Mas e se o projeto em questão depender de outros serviços, havendo ainda a necessidade de uso de containers com todas as partes envolvidas?
Num cenário mais simplista, criaríamos containers um a um, efetuando também os ajustes de configuração necessários para que a aplicação principal acesse corretamente cada serviço. Embora eficaz, esta abordagem pode se mostrar improdutiva em projetos complexos que envolvam diversas dependências. A geração individual de vários containers durante um deployment é um procedimento trabalhoso e, sem sombra de dúvidas, sujeito a erros.
Como simplificar todo este processo?
A resposta para esta pergunta está no Docker Compose, um serviço do próprio Docker voltado à criação e execução conjunta dos múltiplos containers de uma aplicação. Tal capacidade facilita o deployment de um projeto em diferentes ambientes. Além disso, o Docker Compose é considerado uma alternativa extremamente útil em cenários que envolvam a implementação de uma arquitetura de micro serviços
Este artigo traz um exemplo de utilização do Docker Compose em conjunto com o ASP.NET↳.NET113 conteúdosNovidades do .NET 9 – o tipo genérico OrderedDictionaryDev (Back & Front) · dez 2024Novidades .NET 10: novas formas de uso do .NET CLIDev (Back & Front) · dez 2025Novidades do .NET 5: implementando um proxy reverso com YARP + ASP.NET 5Dev (Back & Front) · nov 2020Ver tudo em Dev (Back & Front) → Core 2.0. Para esta aplicação, serão criados dois containers Docker: um contendo o próprio site correspondente ao projeto, enquanto no segundo estará uma instância do Redis empregada no armazenamento de dados em cache.
Implementando a aplicação para testes
O projeto descrito neste artigo (SiteHeroisMarvel) está baseado no uso de Razor Pages, uma nova estrutura para implementação de soluções Web introduzidas com o ASP.NET Core 2.0. Esta aplicação acessará a API gratuita da Marvel Comics, armazenando em cache dados sobre super-heróis através do Redis (solução NoSQL↳NoSQL9 conteúdosSQL ou NoSQL: eis a questão!!Data · mar 2020DBA Developers Summit 2019: O que você precisa saber para modelar bancos de dados NoSQLData · mai 2019ElasticSearch… Duas coisas que você precisa saber!Data · mai 2020Ver tudo em Data → e open source do tipo chave-valor).
Uma implementação similar a esta já foi detalhada no seguinte artigo:
As fontes do projeto SiteHeroisMarvel (criado a partir do Visual Studio Code) já se encontram no GitHub:
Na listagem a seguir, é possível observar que o uso de cache com Redis foi ativado no método ConfigureServices da classe Startup, com isto acontecendo por meio de uma chamada à operação AddDistributedRedisCache:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace SiteHeroisMarvel
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
// Ativando o uso de cache via Redis
services.AddDistributedRedisCache(options =>
{
options.Configuration =
Configuration.GetConnectionString("ConexaoRedis");
options.InstanceName = "TesteRedisCache";
});
services.AddMvc();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
}
app.UseStaticFiles();
app.UseMvc();
}
}
}
O tipo Personagem contém as informações que serão exibidas sobre heróis Marvel (e também vinculadas ao cache):
namespace SiteHeroisMarvel
{
public class Personagem
{
public string Nome { get; set; }
public string Descricao { get; set; }
public string UrlImagem { get; set; }
public string UrlWiki { get; set; }
}
}
Já na listagem a seguir está o código para a exibição de dados na View Index.cshtml:
@page
@model IndexModel
@{
ViewData["Title"] = "Home Page";
Personagem personagem = (Personagem)TempData["Personagem"];
}
<div style="overflow: hidden; margin-top: 50px;">
<div style="float: left; margin-right: 50px;">
<img src="@personagem.UrlImagem" height="200" width="200" />
</div>
<div style="margin-left: 50px;">
<h2><a href="@personagem.UrlWiki">@personagem.Nome</a></h2>
<p>@personagem.Descricao</p>
</div>
</div>
E a próxima listagem traz a implementação do método OnGet para o arquivo Index.cshtml.cs, com o acesso à API REST da Marvel Comics:
using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Security.Cryptography;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Caching.Distributed;
using Newtonsoft.Json;
namespace SiteHeroisMarvel.Pages
{
public class IndexModel : PageModel
{
private static object syncObject = Guid.NewGuid();
private static string[] HEROIS = new string[]
{
"Captain America", "Iron Man", "Thor", "Hulk",
"Wolverine", "Spider-Man", "Black Panther",
"Doctor Strange", "Daredevil"
};
public void OnGet(
[FromServices]IConfiguration config,
[FromServices]IDistributedCache cache)
{
Personagem personagem = null;
string heroi = HEROIS[new Random().Next(0, 9)];
string valorJSON = cache.GetString(heroi);
if (valorJSON == null)
{
// Exemplo de implementação do pattern Double-checked locking
// Para mais informações acesse:
// https://en.wikipedia.org/wiki/Double-checked_locking
lock (syncObject)
{
valorJSON = cache.GetString(heroi);
if (valorJSON == null)
{
personagem = ObterPersonagem(config, heroi);
DistributedCacheEntryOptions opcoesCache =
new DistributedCacheEntryOptions();
opcoesCache.SetAbsoluteExpiration(
TimeSpan.FromMinutes(20));
valorJSON = JsonConvert.SerializeObject(personagem);
cache.SetString(heroi, valorJSON, opcoesCache);
}
}
}
if (personagem == null && valorJSON != null)
{
personagem = JsonConvert
.DeserializeObject<Personagem>(valorJSON);
}
TempData["Personagem"] = personagem;
}
private Personagem ObterPersonagem(
IConfiguration config, string heroi)
{
using (var client = new HttpClient())
{
client.DefaultRequestHeaders.Accept.Clear();
client.DefaultRequestHeaders.Accept.Add(
new MediaTypeWithQualityHeaderValue("application/json"));
string ts = DateTime.Now.Ticks.ToString();
string publicKey = config.GetSection("MarvelComicsAPI:PublicKey").Value;
string hash = GerarHash(ts, publicKey,
config.GetSection("MarvelComicsAPI:PrivateKey").Value);
string url = config.GetSection("MarvelComicsAPI:BaseURL").Value +
$"characters?ts={ts}&apikey={publicKey}&hash={hash}&" +
$"name={Uri.EscapeUriString(heroi)}";
HttpResponseMessage response = client.GetAsync(
url).Result;
response.EnsureSuccessStatusCode();
string conteudo =
response.Content.ReadAsStringAsync().Result;
dynamic resultado = JsonConvert.DeserializeObject(conteudo);
Personagem personagem = new Personagem();
personagem.Nome = resultado.data.results[0].name;
personagem.Descricao = resultado.data.results[0].description;
personagem.UrlImagem = resultado.data.results[0].thumbnail.path + "." +
resultado.data.results[0].thumbnail.extension;
personagem.UrlWiki = resultado.data.results[0].urls[1].url;
return personagem;
}
}
private string GerarHash(
string ts, string publicKey, string privateKey)
{
byte[] bytes =
Encoding.UTF8.GetBytes(ts + privateKey + publicKey);
var gerador = MD5.Create();
byte[] bytesHash = gerador.ComputeHash(bytes);
return BitConverter.ToString(bytesHash)
.ToLower().Replace("-", String.Empty);
}
}
}
Por fim, temos ainda a string de conexão para uso do Redis no arquivo appsettings.json. Ao invés de um host e sua respectiva porte de acesso – algo como localhost:6379 – foi informada na mesma o nome do container que o Docker Compose irá gerar (maiores detalhes sobre esta característica serão discutidos nas próximas seções):
{
"ConnectionStrings": {
"ConexaoRedis": "redisheroismarvel"
},
"MarvelComicsAPI": {
"BaseURL": "http://gateway.marvel.com/v1/public/",
"PublicKey": "CHAVE PÚBLICA",
"PrivateKey": "CHAVE PRIVADA"
},
"Logging": {
"IncludeScopes": false,
"LogLevel": {
"Default": "Warning"
}
}
}
Criando os arquivos Dockerfile e docker-compose.yaml
Deverão ser criados também, os arquivos Dockerfile e docker-compose.yaml, a fim de possibilitar a geração dos containers esperados via Docker Compose. O Visual Studio Code conta inclusive, com suporte aos formatos utilizados por estes itens (no caso específico de arquivos do Docker Compose o padrão/formato adotado é conhecido como YAML):

O arquivo Dockerfile será empregado na geração da imagem Docker do projeto SiteHeroisMarvel. As instruções necessárias para a montagem dessa estrutura estão indicadas na próxima listagem:
FROM microsoft/aspnetcore-build:2.0 AS build-env WORKDIR /app # Copiar csproj e restaurar dependencias COPY *.csproj ./ RUN dotnet restore # Build da aplicacao COPY . ./ RUN dotnet publish -c Release -o out # Build da imagem FROM microsoft/aspnetcore:2.0 WORKDIR /app COPY --from=build-env /app/out . ENTRYPOINT ["dotnet", "SiteHeroisMarvel.dll"]
Quanto ao docker-compose.yaml, é possível observar neste arquivo as seguintes seções:
- services, com os diferentes serviços a serem criados por meio de containers (siteheroismarvel e redisheroismarvel);
- networks, com as configurações para a criação de uma nova network/rede (marvel-network). Será justamente esta configuração que permitirá ao container da aplicação SiteHeroisMarvel acessar o container Redis sem a necessidade de indicação de uma porta (esta característica já foi ressaltada anteriormente ao se definir a string de conexão no arquivo appsettings.json).
No caso do serviço siteheroismarvel foram especificados o nome da imagem (image), o mapeamento entre as portas (ports), a rede empregada (networks) e a dependência para com o container redisheroismarvel (depends_on). Além disso, a presença do item build indicará a necessidade de se proceder com a geração de uma imagem antes da criação do container correspondente.
Para o container redisheroismarvel, foram configurados o nome da imagem (image, a qual será baixada caso ainda não exista na máquina de testes), o mapeamento entre portas (ports) e a rede (networks).
version: '3'
services:
siteheroismarvel:
image: siteheroismarvel
build:
context: .
ports:
- "20000:80"
networks:
- marvel-network
depends_on:
- "redisheroismarvel"
redisheroismarvel:
image: redis:alpine
ports:
- "16379:6379"
networks:
- marvel-network
networks:
marvel-network:
driver: bridge
Executando o Docker Compose e testando a aplicação
Para iniciar a execução do projeto de testes via Docker Compose, será necessário agora acessar o diretório inicial do mesmo, através de uma ferramenta de linha de comandos (no caso de um sistema Windows, uma opção é o PowerShell). Acionar na sequência o comando:
docker-compose up -d
Essa instrução fará com que as imagens necessárias sejam criadas e/ou baixadas. Montará ainda, a rede especificada em docker-compose.yaml, e finalmente irá gerar e iniciar em modo background os containers esperados para a aplicação:


O comando docker images exibirá as imagens siteheroismarvel:latest e redis:alpine, as quais serviram de base para a geração dos containers indicados em docker-compose.yaml:

Já o comando docker network ls listará a rede marvel-network entre as networks disponíveis:

A instrução docker-compose ps trará os containers gerados por meio do comando docker-compose up (os mesmos se encontram em execução nas portas 20000 e 16379):

Ao acessar a porta 20000 via browser, serão exibidos resultados das consultas à API da Marvel Comics:



Uma conexão à porta 16379 via Redis Desktop Manager trará as chaves armazenadas no container Redis:


A remoção dos containers e da network gerados durante os testes é uma tarefa muito simples, bastando para isto acionar o comando docker-compose down:

E para concluir este artigo, deixo aqui a gravação de um evento recente do Canal .NET focado em Docker Compose:







