WeatherForecast. Docker-изация.
Предисловие
И так, у нас есть проект, подопытный кролик, который мы опубликовали нашими умелыми ручками на виртуальную машину с "настоящими" базами данных Postgres и Redis. В очевидной простоте (скачать исходники на виртуальную машину, собрать, настроить демона) есть и такие же очевидные недостатки:
- ручная работа (= ошибки), которую можно автоматизировать скриптами. Но главное достоинство хорошего программиста - это лень. Если можно что-то не делать, лучше этого не делать.
- сложности с обновлением приложения. Вот вы запустили первую версию. Прошло время и у вас готова новая версия приложения, а денег вы ещё не заработали на новые сервера. Вам придется производить работы на "горячем" сервере. Огромная доза адреналина вам обеспечена. Сервер будет обслуживать клиентов и в это же время собирать новую версию. А ещё нужно внести изменения в настройки системы и NGINX. Страшно! Логично остановить работу приложения на время работ. И скорее всего вам придется это сделать, так как новая версия потребует обновление схемы баз данных. А делать миграции баз данных так, чтобы не требовалось останавливать приложение нужно уметь ещё. И это пол беды. А что если в новой версии обнаружились баги?! o_0 Нужно откатываться на предыдущую версию (вы же не удалили ветку в git с предыдущей версией o_0). Опять останов. Куча адреналина!
- и самое волшебное, что может с вами случиться - проект просто не соберется на сервере. Да, да. Такое может случиться. Например, вы забили закомитить какой-то файл. Или GitHub или какой-то другой репозиторий прилег, или забанен. Или сервер NuGet решил взять выходной. Или...
Тут-то и приходит нам на помощь Docker. Но давайте по порядку. Сначала нам нужно поместить наше приложение в контейнер и научиться работать с ним локально.
Поехали
Исходники к этому посту находятся тут.
Начнем мы с того, что удалим контейнеры Postgres и Redis, который ранее создавали. Наше приложение будет запускаться в контейнере и подключаться оно будет к базам данных, который тоже находятся в контейнерах. Настройка сети Docker-а, которая используется по-умолчанию, сеть bridge, позволяет адресовать контейнеры только через IP, что не удобно (нужно будет после запуска контейнера узнавать его IP). Подробности тут. По этому мы сначала создадим свою сеть в Docker командой:
docker network create weather-forecast-net
Команды для создания контейнеров баз данных теперь будут выглядеть так (мы добавили параметр --net):
docker run --name weather-forecast-postgres --net weather-forecast-net -p 5432:5432 -e POSTGRES_PASSWORD=postgres -d postgres
docker run --name weather-forecast-redis --net weather-forecast-net -p 6379:6379 -d redis
Для создания образа контейнера нашего приложения нужно создать в проекте файл Dockerfile:
FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build # Copy everything COPY . ./ # Build and publish a release RUN dotnet publish DeployToProduction.WeatherForecast.App --configuration Release -o out # Build runtime image FROM mcr.microsoft.com/dotnet/aspnet:7.0 EXPOSE 80 COPY --from=build /out . # Set Environment Variables for the App ENV ASPNETCORE_ENVIRONMENT=Production ENV DOTNET_PRINT_TELEMETRY_MESSAGE=false ENV ConnectionStrings__Postgres="Host=weather-forecast-postgres;Username=postgres;Password=postgres;Database=postgres" ENV ConnectionStrings__Redis="weather-forecast-redis:6379" # Start App ENTRYPOINT ["dotnet", "DeployToProduction.WeatherForecast.App.dll"]
Сначала мы используем образ "mcr.microsoft.com/dotnet/sdk:7.0" для сборки и публикации проекта: копируем исходники инструкцией COPY, запускаем сборку dotnet инструкцией RUN.
Берем второй образ "mcr.microsoft.com/dotnet/aspnet:7.0", в котором есть только run-time dotnet-а и apt.net, копируем в него результат сборки-публикации, то есть наше приложение, задаем переменные окружения инструкцией ENV, и указываем какую команду должен выполнить контейнер при старте инструкцией ENTRYPOINT.
Обратите внимание, как мы задаем строки подключения, имена серверов баз данных - это имена контейнеров (для этого мы и создавали свою сеть в Docker).
Дополнительно, чтобы в образы не копировались "ненужные" файлы, радом с Dockerfile лежит файл .dockerignore - это список файлов и каталогов, который нужно проигнорировать при сборке образа.
Теперь открываем терминал в каталоге где лежит Dockerfile и выполняем команду сборки образа:
docker build -t weather-forecast-app-img -f Dockerfile .
Новый образ вы увидите в приложении Docker Desktop:
Осталось запустить контейнер из этого образа (образы баз данных должны быть запущены):
docker run --name weather-forecast-app --net weather-forecast-net -p 5022:80 -d weather-forecast-app-img
Подытожим. Теперь мы можем оперировать Docker-образами как единицами деплоймента. Один раз собрали образ контейнера - теперь он запускается и работает везде одинаково. "Развернуть" приложение теперь - это одна команда запуска Docker-контейнера. Образы хранятся и версионируются в специальных репозиториях.
З.Ы.
А теперь самое вкусненькое =) Помните, ранее для UI теста мы сначала запускали само приложение, а потом запускали тест. Теперь мы можем убрать это неудобство предоставив работу по запуску приложения библиотеке Testcontainers (у нас же теперь есть образ нашего приложения).