В этой статье мы подготовим окружение для запуска контейнеров в Windows 10 и создадим простое контейнеризированное .NET приложение
Чтобы все описанные ниже действия были успешно выполнены,
потребуется 64-разрядная система с версией не меньше 2004 и сборкой
не меньше 18362. Проверим версию и номер сборки, выполнив в
PowerShell команду winver
Если версия ниже требуемой, то необходимо произвести обновление и только после этого идти дальше
Установка WSL 2
Сначала включим компонент Windows Subsystem for Linux (WSL). Для этого запустим PowerShell с правами администратора и выполним первую команду
dism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart
Выполним следующую команду
dism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart
Чтобы завершить установку, перезагрузим компьютер shutdown
-r -t 1
Установим пакет обновления ядра Linux
Выберем WSL 2 по умолчанию для новых дистрибутивов Linux
wsl --set-default-version 2
Для целей этой статьи это необязательно, но установим дистрибутив Linux через Microsoft Store, например, Ubuntu 20.04 LTS
При первом запуске установленного дистрибутива введем имя пользователя и пароль
Чтобы увидеть запущенные дистрибутивы Linux, выполним в
PowerShell команду wsl --list --verbose
Чтобы завершить работу дистрибутива Linux, выполним команду
wsl --terminate Ubuntu-20.04
Файловая система запущенного дистрибутива Linux будет
смонтирована по этому пути \\wsl$
Более подробно о подсистеме WSL
Более подробно об установке подсистемы WSL и устранение неполадок
Установка Docker
Скачаем Docker Desktop для Windows и установим, следуя простым инструкциям
После установки запустим приложение Docker Desktop и установим интеграцию Docker с дистрибутивом Linux (WSL 2)
Теперь отправлять команды Docker можно как через PowerShell, так
и через Bash. Выполним команду docker version
Более подробно о Docker Desktop
Запуск контейнеров
Чтобы убедиться, что Docker правильно установлен и работает должным образом, запустим простой контейнер busybox, который всего лишь выведет в консоль переданное сообщение и завершит свое выполнение
docker run busybox echo "hello docker!!!"
Хорошо. Давайте сделаем что-то более интересное. Например, запустим контейнер rabbitmq
docker run --name rabbit1 -p 8080:15672 -p 5672:5672 rabbitmq:3.8.9-management
Разберем выполненную команду:
docker run
- запускает контейнер из образа. Если
данный образ отсутствует локально, то предварительно он будет
загружен из репозитория Docker Hub
--name rabbit1
- присваивает запускаемому
контейнеру имя rabbit1
-p 8080:15672
- пробрасывает порт с хоста в
контейнер. 8080 - порт на хосте, 15672 - порт в контейнере
rabbitmq:3.8.9-management
- имя образа и его
тег/версия, разделенные двоеточием
Теперь мы можем извне контейнера взаимодействовать с сервером RabbitMQ через порт 5672 и получить доступ к управлению из браузера через порт 8080
Посмотреть статус контейнеров, в том числе остановленных, можно
с помощью команд docker container ls --all
или
docker ps -a
Чтобы остановить наш контейнер: docker stop
rabbit1
. Запустить вновь: docker start
rabbit1
Более подробно о командах Docker
Отладка .NET приложения запущенного в контейнере
Для нашего примера нам понадобится отдельная сеть, т.к. мы запустим целых два контейнера, которые будут взаимодействовать между собой. На самом деле все запускаемые контейнеры по умолчанию попадают в уже существующую сеть с именем bridge, но т.к. в своей сети мы без лишних проблем сможешь обращаться из одного контейнера к другому прямо по имени, создадим сеть с названием mynet типа bridge
docker network create mynet
Далее запустим redis и подключим его к ранее
созданной сети. Благодаря параметру -d
процесс в
контейнере будет запущен в фоновом режиме
docker run --name redis1 --network mynet -d redis
Далее с помощью Visual Studio 2019 создадим новый проект ASP.NET Core Web API, который будет использован для демонстрации отладки
Добавим для взаимодействия с Redis пакет StackExchange.Redis через Package Manager Console
Install-Package StackExchange.Redis -Version 2.2.4
Мы не будем акцентироваться на правильности и красоте дизайна, а быстро создадим рабочий пример
Добавим в проект файл RandomWeatherService.cs, где будет находится служба для выдачи не очень точного прогноза
using System;namespace WebApiFromDocker{ public class RandomWeatherService { private Random _randomGenerator; public RandomWeatherService() { _randomGenerator = new Random(); } public int GetForecast(string city) { var length = city.Length; var temperatureC = _randomGenerator.Next(-length, length); return temperatureC; } }}
Добавим файл RedisRepository.cs, где будет находится служба кеширования сформированных прогнозов
using StackExchange.Redis;using System;using System.Threading.Tasks;namespace WebApiFromDocker{ public class RedisRepository { private string _connectionString = "redis1:6379"; private TimeSpan _expiry = TimeSpan.FromHours(1); public async Task SetValue(string key, string value) { using var connection = await ConnectionMultiplexer .ConnectAsync(_connectionString); var db = connection.GetDatabase(); await db.StringSetAsync(key.ToUpper(), value, _expiry); } public async Task<string> GetValue(string key) { using var connection = await ConnectionMultiplexer .ConnectAsync(_connectionString); var db = connection.GetDatabase(); var redisValue = await db.StringGetAsync(key.ToUpper()); return redisValue; } }}
Зарегистрируем созданные службы в классе Startup
public void ConfigureServices(IServiceCollection services){ services.AddScoped<RandomWeatherService>(); services.AddScoped<RedisRepository>(); services.AddControllers();}
И наконец, изменим созданный автоматически единственный контроллер WeatherForecastController следующим образом
using Microsoft.AspNetCore.Mvc;using System;using System.Threading.Tasks;namespace WebApiFromDocker.Controllers{ [ApiController] [Route("api/[controller]")] public class WeatherForecastController : ControllerBase { private RandomWeatherService _weather; private RedisRepository _cache; public WeatherForecastController( RandomWeatherService weather, RedisRepository cache) { _weather = weather; _cache = cache; } //GET /api/weatherforecast/moscow [HttpGet("{city}")] public async Task<WeatherForecast> GetAsync(string city) { int temperatureC; var cachedTemperatureCString = await _cache.GetValue(city); if (!string.IsNullOrEmpty(cachedTemperatureCString)) { temperatureC = Convert.ToInt32(cachedTemperatureCString); } else { temperatureC = _weather.GetForecast(city); await _cache.SetValue(city, temperatureC.ToString()); } var forecast = new WeatherForecast( city, DateTime.UtcNow, temperatureC); return forecast; } }}
Помимо прочего в проект автоматически был добавлен файл Dockerfile с инструкциями для Docker. Оставим его без изменений
FROM mcr.microsoft.com/dotnet/aspnet:5.0-buster-slim AS baseWORKDIR /appEXPOSE 80FROM mcr.microsoft.com/dotnet/sdk:5.0-buster-slim AS buildWORKDIR /srcCOPY ["WebApiFromDocker/WebApiFromDocker.csproj", "WebApiFromDocker/"]RUN dotnet restore "WebApiFromDocker/WebApiFromDocker.csproj"COPY . .WORKDIR "/src/WebApiFromDocker"RUN dotnet build "WebApiFromDocker.csproj" -c Release -o /app/buildFROM build AS publishRUN dotnet publish "WebApiFromDocker.csproj" -c Release -o /app/publishFROM base AS finalWORKDIR /appCOPY --from=publish /app/publish .ENTRYPOINT ["dotnet", "WebApiFromDocker.dll"]
В результате получим следующую структуру проекта
Если по какой-то невероятной причине Вам понадобятся исходники, то они здесь
Запустим наше приложение в контейнере под отладкой
После того как контейнер будет запущен, также подключим его к сети mynet
docker network connect mynet WebApiFromDocker
После убедимся, что все необходимые контейнеры находятся в одной сети
docker network inspect mynet
Далее установим Breakpoint в единственном методе контроллера и пошлем запрос через Postman, или через любой браузер
http://localhost:49156/api/weatherforecast/moscow
Кстати, используемый порт в Вашем случае может отличаться и его можно посмотреть в окне Containers
Результат в окне Postman
Дополнительно убедимся, что значение зафиксировано в redis, подключившись с помощью консоли redis-cli
Хорошо, все сработало, как и задумано!