<?xml version="1.0" encoding="utf-8" ?><rss version="2.0" xmlns:tt="http://teletype.in/" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:media="http://search.yahoo.com/mrss/"><channel><title>Анатолий Северин</title><generator>teletype.in</generator><description><![CDATA[.NET разработчик. Рассказываю про деплой в прод.]]></description><image><url>https://img1.teletype.in/files/87/26/87269c4a-9e0a-419b-8549-a87e34c2ac12.png</url><title>Анатолий Северин</title><link>https://deploy2production.ru/</link></image><link>https://deploy2production.ru/?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=deploy2production</link><atom:link rel="self" type="application/rss+xml" href="https://teletype.in/rss/deploy2production?offset=0"></atom:link><atom:link rel="next" type="application/rss+xml" href="https://teletype.in/rss/deploy2production?offset=10"></atom:link><atom:link rel="search" type="application/opensearchdescription+xml" title="Teletype" href="https://teletype.in/opensearch.xml"></atom:link><pubDate>Mon, 27 Apr 2026 08:52:08 GMT</pubDate><lastBuildDate>Mon, 27 Apr 2026 08:52:08 GMT</lastBuildDate><item><guid isPermaLink="true">https://deploy2production.ru/weather-forecast-ads-docker</guid><link>https://deploy2production.ru/weather-forecast-ads-docker?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=deploy2production</link><comments>https://deploy2production.ru/weather-forecast-ads-docker?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=deploy2production#comments</comments><dc:creator>deploy2production</dc:creator><title>WeatherForecast. Ads. Docker.</title><pubDate>Tue, 06 Jun 2023 09:38:18 GMT</pubDate><media:content medium="image" url="https://img4.teletype.in/files/fc/29/fc29e9d0-38f3-4b83-acd5-374a761bcb4a.png"></media:content><category>Weather Forecast</category><description><![CDATA[<img src="https://img1.teletype.in/files/c4/b7/c4b7cba4-1e60-4615-9e0d-1383983df30a.png"></img>О следующем шаге вы уже догадались. Нужно новый сервис &quot;упаковать&quot; в  docker-контейнер.]]></description><content:encoded><![CDATA[
  <h2 id="nGFb">Предисловие</h2>
  <p id="DRIO">О следующем шаге вы уже догадались. Нужно новый сервис &quot;упаковать&quot; в  docker-контейнер.</p>
  <h2 id="XmLJ">Поехали</h2>
  <p id="7Lv6">Как всегда, код <a href="https://github.com/deploy2production/weather-forecast/tree/ads-docker" target="_blank">тут</a>.</p>
  <p id="f842">Первым делом мы создадим docker-файл (<em>Dockerfile.ads</em>) с инструкцией по сборке контейнера, он не будет сольно отличаться от docker-файла для основного приложения:</p>
  <pre id="SjB5" data-lang="dockerfile">FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build

# Copy everything
COPY . ./


# Build and publish a release
RUN dotnet publish DeployToProduction.Ads.WebApi --configuration Release -o out

# Build runtime image
FROM mcr.microsoft.com/dotnet/aspnet:7.0
EXPOSE 80

COPY --from=build /out .

# Start App
ENTRYPOINT [&quot;dotnet&quot;, &quot;DeployToProduction.Ads.WebApi.dll&quot;]</pre>
  <p id="tlNg">Далее у нас несколько вариантов.</p>
  <p id="LKlW">Первый - это собрать образ отдельной командой:</p>
  <pre id="khJ5" data-lang="shell">docker build -t weather-forecast-ads-img -f Dockerfile.ads .</pre>
  <figure id="fNp5" class="m_original">
    <img src="https://img1.teletype.in/files/c4/b7/c4b7cba4-1e60-4615-9e0d-1383983df30a.png" width="1213" />
  </figure>
  <p id="SZ3D">Второй вариант - обновить файлы docker-compose и пересобрать все образы.</p>
  <p id="lDp7">Как вы помните, у нас был один файл docker-compose для конфигурации Development - <em>docker-compose.app.dev.yml</em>:</p>
  <pre id="wXqr" data-lang="yaml">version: &quot;3.9&quot;

services:
  ads:
    container_name: &quot;weather-forecast-ads&quot;
    image: &quot;weather-forecast-ads-img&quot;
    build:
      context: .
      dockerfile: Dockerfile.ads
    networks:
      - weather-forecast-net
    ports:
      - &quot;5298:80&quot;
    environment:
       ASPNETCORE_ENVIRONMENT: Development
       DOTNET_PRINT_TELEMETRY_MESSAGE: false
  app:
    container_name: &quot;weather-forecast-app&quot;
    image: &quot;weather-forecast-app-img&quot;
    build:
      context: .
      dockerfile: Dockerfile.app
    networks:
      - weather-forecast-net
    ports:
      - &quot;5022:80&quot;
    environment:
       ASPNETCORE_ENVIRONMENT: Development
       DOTNET_PRINT_TELEMETRY_MESSAGE: false
       ConnectionStrings__Postgres: Host=weather-forecast-postgres;User Id=postgres;Password=postgres;Database=postgres
       ConnectionStrings__Redis: weather-forecast-redis:6379
       ConnectionStrings__AdsServerUrl: http://weather-forecast-ads
    depends_on:
      postgres:
        condition: service_started
      redis:
        condition: service_started
      ads:
        condition: service_started</pre>
  <p id="6xxO">Второй - для конфигурации Production - <em>docker-compose.app.prd.yml</em>:</p>
  <pre id="EbcP" data-lang="yaml">version: &quot;3.9&quot;

services:
  ads:
    container_name: &quot;weather-forecast-ads&quot;
    image: &quot;weather-forecast-ads-img&quot;
    build:
      context: .
      dockerfile: Dockerfile.ads
    networks:
      - weather-forecast-net
    ports:
      - &quot;5298:80&quot;
    environment:
       ASPNETCORE_ENVIRONMENT: Production
       DOTNET_PRINT_TELEMETRY_MESSAGE: false
  setup:
    container_name: &quot;weather-forecast-setup&quot;
    image: &quot;weather-forecast-setup-img&quot;
    build:
      context: .
      dockerfile: Dockerfile.setup
    networks:
      - weather-forecast-net
    environment:
       ConnectionStrings__Postgres: Host=weather-forecast-postgres;User Id=postgres;Password=postgres;Database=postgres
    depends_on:
      postgres:
        condition: service_started
      redis:
        condition: service_started
  app:
    container_name: &quot;weather-forecast-app&quot;
    image: &quot;weather-forecast-app-img&quot;
    build:
      context: .
      dockerfile: Dockerfile.app
    networks:
      - weather-forecast-net
    ports:
      - &quot;5022:80&quot;
    environment:
       ASPNETCORE_ENVIRONMENT: Production
       DOTNET_PRINT_TELEMETRY_MESSAGE: false
       ConnectionStrings__Postgres: Host=weather-forecast-postgres;User Id=webapp;Password=webapppwd;Database=postgres
       ConnectionStrings__Redis: weather-forecast-redis:6379
       ConnectionStrings__AdsServerUrl: http://weather-forecast-ads
    depends_on:
      setup:
        condition: service_completed_successfully</pre>
  <p id="fn59">Обращу ваше внимание на один момент. В Dockerfile.ads мы указываем инструкцией <strong>EXPOSE 80</strong>, что контейнер из этого образа &quot;выставляет&quot; порт 80, то есть слушает и принимает запросы по этому порту. В docker-compose файле при описании сервиса <strong>ads </strong>в параметре <strong>ports </strong>мы указываем какой порт &quot;хоста&quot; будет использоваться для обращения к <strong>80</strong>-му порту контейнера. <u>НО</u> в переменной окружения <strong>ConnectionStrings__AdsServerUrl </strong>для сервиса <strong>app </strong>мы указываем адрес сервиса <a href="http://weather-forecast-ads" target="_blank">http://weather-forecast-ads</a>, где weather-forecast-ads - это имя контейнера, порт мы не указываем, так как используется 80-ый порт по умолчанию. Акцентирую, <u>не правильными</u> вариантами будут: <a href="http://localhost:5298" target="_blank">http://localhost:5298</a>, <a href="http://weather-forecast-ads:5298" target="_blank">http://weather-forecast-ads:5298</a>.</p>
  <p id="XZMB">Собрать образы и запустить все сервисы вы можете одной из команд:</p>
  <pre id="S4Jn" data-lang="shell">docker compose -f docker-compose.db.yml -f docker-compose.app.dev.yml up --build</pre>
  <pre id="oYZQ" data-lang="shell">docker compose -f docker-compose.db.yml -f docker-compose.app.prd.yml up --build</pre>
  <p id="fHqe"> Проверим что все образы на месте в Docker Desktop:</p>
  <figure id="59fW" class="m_original">
    <img src="https://img1.teletype.in/files/85/ab/85ab1a0b-a2d2-4268-8fe4-18fbc55041be.png" width="1270" />
  </figure>
  <p id="ei2B">Протестируем работу нашего приложения:</p>
  <figure id="3VNb" class="m_original">
    <img src="https://img4.teletype.in/files/bb/60/bb608118-8bb5-4d9a-ad59-25c8d12dc6ee.png" width="772" />
  </figure>
  <figure id="hMwp" class="m_original">
    <img src="https://img4.teletype.in/files/b3/96/b3965b7a-74e0-40ff-bd36-5d43e14d84bb.png" width="1086" />
  </figure>
  <p id="47Kk"></p>
  <p id="zrdI">Освежим в памяти, как загрузить образ в <em>Container Registry</em>, на примере нового образа для ads. Для этого нужно сначала добавить тэг:</p>
  <pre id="G4G7" data-lang="shell">docker tag weather-forecast-ads-img cr.selcloud.ru/deploy2production/weather-forecast-ads-img:v1.0</pre>
  <p id="W4Zi">А потом отправить образ в <em>Container Registry</em>:</p>
  <pre id="20w8" data-lang="shell"> docker push cr.selcloud.ru/deploy2production/weather-forecast-ads-img:v1.0</pre>
  <figure id="9eWH" class="m_original">
    <img src="https://img3.teletype.in/files/26/bb/26bb5934-bb2b-4d74-bb8b-286a39c516e3.png" width="1501" />
  </figure>
  <p id="L27B">Проверяем список образов в интерфейсе Selectel:</p>
  <figure id="7Cy0" class="m_original">
    <img src="https://img4.teletype.in/files/33/23/3323a217-5955-4f61-b5d4-99db20d10937.png" width="1154" />
  </figure>
  <p id="3I8w">Текущий вариант приложения мы не будем разворачивать в Selectel. Вы можете потренироваться самостоятельно, но для этого не забудьте обновить образ приложения <strong>app </strong>в <em>Container Registry</em> с новой версией кода. Далее мы добавим в сервис A<strong>ds </strong>базу данных.</p>
  <h2 id="WVg7">З.Ы.</h2>
  <p id="Mmzc">Традиционное заключение - автоматизированные тесты. Добавляем сборку для интеграционного тестирования сервиса <strong>Ads</strong>:</p>
  <figure id="8Do2" class="m_original">
    <img src="https://img2.teletype.in/files/9b/55/9b557788-0404-4eb4-bbcd-9e28af59e782.png" width="466" />
  </figure>
  <p id="BLED">Сервис запускается в контейнере с помощью библиотеки <em>Testcontainers</em>:</p>
  <figure id="F3eJ" class="m_original">
    <img src="https://img4.teletype.in/files/f5/16/f51631fd-33bf-458b-8127-47a32f5273c8.png" width="932" />
  </figure>
  <p id="mgvv">Я выделил интересный момент - это ожидание, когда сервис запустится и будет готов отвечать на наши запросы. Для этого в сам сервис <strong>Ads </strong>я добавил контроллер, который называют health check:</p>
  <figure id="C08p" class="m_original">
    <img src="https://img4.teletype.in/files/bd/9d/bd9d672a-e9a7-4c89-a54d-37bd591f9810.png" width="545" />
  </figure>
  <p id="CX8f"> Так же мы добавляем в UI-тест приложения проверку блока с рекламой:</p>
  <figure id="YFNf" class="m_original">
    <img src="https://img1.teletype.in/files/c3/96/c396a199-53a4-4191-a338-bb13a1b1dd29.png" width="999" />
  </figure>

]]></content:encoded></item><item><guid isPermaLink="true">https://deploy2production.ru/weather-forecast-ads</guid><link>https://deploy2production.ru/weather-forecast-ads?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=deploy2production</link><comments>https://deploy2production.ru/weather-forecast-ads?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=deploy2production#comments</comments><dc:creator>deploy2production</dc:creator><title>WeatherForecast. Сервисы. Dev.</title><pubDate>Sun, 30 Apr 2023 19:38:03 GMT</pubDate><media:content medium="image" url="https://img4.teletype.in/files/35/ec/35ec292a-fb36-4f15-ad9f-38a52494d705.png"></media:content><category>Weather Forecast</category><description><![CDATA[<img src="https://img1.teletype.in/files/89/bb/89bbee97-c470-4704-b658-7d19f9ea01bc.png"></img>Мы начали с простенького веб-приложения и разобрали базовые шаги в его публикации. Теперь пришло время немного усложнить наш учебный пример. В наши дни реальные проекты редко (читай &quot;никогда&quot;) состоят только из одного сервиса, тем более когда так модны &quot;микросервисы&quot;. Поэтому дальше мы разберемся как разрабатывать и публиковать много-модульные/сервисные проекты.]]></description><content:encoded><![CDATA[
  <h2 id="3ikQ">Предисловие</h2>
  <p id="EHPo">Мы начали с простенького веб-приложения и разобрали базовые шаги в его публикации. Теперь пришло время немного усложнить наш учебный пример. В наши дни реальные проекты редко (читай &quot;никогда&quot;) состоят только из одного сервиса, тем более когда так модны &quot;микросервисы&quot;. Поэтому дальше мы разберемся как разрабатывать и публиковать много-модульные/сервисные проекты.</p>
  <h2 id="Na7q">Поехали</h2>
  <p id="3rY6">Как всегда, код <a href="https://github.com/deploy2production/weather-forecast/tree/ads-project" target="_blank">тут</a>. </p>
  <p id="vNgb">Нашему незамысловатому проекту нужна функциональность, которую мы могли бы вынести в отдельно стоящий сервис. Давайте показывать &quot;рекламу&quot;, когда пользователь узнает погоду. При этом содержание рекламы будет зависеть от введенного города. Назовем этот сервис Ads.</p>
  <p id="g9E0">Я для удобства создам новый проект в том же solution, но представляя себе, что это два разных &quot;микросервиса&quot;, сделаю их код не зависимым друг от друга. Так бы и произошло, если бы реальный большой проект разрабатывался несколькими команда, каждая из которых отвечала за определенные микросервисы (или только один). Для наглядности я разложу проекты по папочкам Ads и WeatherForecast:</p>
  <figure id="gRH9" class="m_original">
    <img src="https://img1.teletype.in/files/89/bb/89bbee97-c470-4704-b658-7d19f9ea01bc.png" width="458" />
  </figure>
  <p id="hVWY">Для сервиса Ads мне понадобятся два проекта:</p>
  <figure id="cWoL" class="m_original">
    <img src="https://img3.teletype.in/files/6f/be/6fbe2d13-5b0f-4208-9568-10a097952457.png" width="454" />
  </figure>
  <p id="H3mk"><strong>DeployToProduction.Ads.Core</strong> - Library проект, в котором у нас будет код моделек</p>
  <figure id="Zfmw" class="m_original">
    <img src="https://img1.teletype.in/files/0a/8b/0a8b266f-17a1-4797-a440-bd06cfbd4f18.png" width="238" />
  </figure>
  <p id="6W0z"><strong>AdPost </strong>и <strong>AdRequest </strong>- простые Data Transfer Object (DTO) классы сериализуемые в JSON.</p>
  <figure id="Secv" class="m_original">
    <img src="https://img1.teletype.in/files/c4/97/c49776ea-ea7f-47be-b1c2-75a565586225.png" width="602" />
  </figure>
  <figure id="wNuC" class="m_original">
    <img src="https://img1.teletype.in/files/0f/45/0f453554-c788-4e39-80cc-f27b97f6acfb.png" width="589" />
  </figure>
  <p id="EflL"><strong>DeployToProduction.Ads.WebApi</strong> - ASP.NET Core Web API проект, который предоставит end-point для получения объектов <strong>AdPost </strong>по запросу с аргументом <strong>AdRequest</strong>.</p>
  <p id="6LIU">Данные для сервиса пока для простоты мы &quot;захардкодим&quot;. Контроллер для обработки запроса:</p>
  <figure id="m5LB" class="m_original">
    <img src="https://img2.teletype.in/files/97/5e/975ee14a-b28d-425b-a431-2e4981670f1a.png" width="1151" />
  </figure>
  <p id="8Mm0">Уберем лишнее из настроек запуска и отключим открывание браузера при старте проекта:</p>
  <figure id="u9LP" class="m_original">
    <img src="https://img2.teletype.in/files/17/4f/174f3db2-0203-45bb-924a-cd2ebf1f3805.png" width="610" />
  </figure>
  <p id="qHRu">И точка входа:</p>
  <figure id="KnJE" class="m_original">
    <img src="https://img3.teletype.in/files/6e/5f/6e5f53d5-ea20-493a-9f17-c7c5429674da.png" width="511" />
  </figure>
  <p id="6Hhb">Вот такой простой сервис. </p>
  <p id="Jp0L">Нужно запустить и протестировать новый сервис. Делаем его в панели отладки активным</p>
  <figure id="widd" class="m_original">
    <img src="https://img4.teletype.in/files/70/21/702188de-ded1-4506-b009-d93de7379b10.png" width="487" />
  </figure>
  <p id="ZGB7">Запускаем:</p>
  <figure id="wVzS" class="m_original">
    <img src="https://img1.teletype.in/files/03/b3/03b304b0-9b69-41b1-83b6-fc331e77d4a8.png" width="1115" />
  </figure>
  <p id="2PUx">Для тестирования &quot;апишек&quot; сегодня есть много инструментов, один из самый популярных - <a href="https://www.postman.com/" target="_blank">Postman</a>. </p>
  <p id="3QBN">Я предпочитаю использовать расширение для VS Code под названием <a href="https://marketplace.visualstudio.com/items?itemName=humao.rest-client" target="_blank">REST Client</a> из-за его простоты. Достаточно создать файл с расширением http и простым синтаксисом описать запросы. В редакторе появятся &quot;кнопочки&quot; <strong>Send Request</strong>, и теперь можно вручную выполнить проверку:</p>
  <figure id="CQAG" class="m_original">
    <img src="https://img2.teletype.in/files/18/96/1896a84f-f02c-47db-9567-34ad6a2af55d.png" width="1024" />
  </figure>
  <p id="XqY8">Файл <strong><em>web-api.http</em></strong> лежит в каталоге <em><strong>Test</strong></em>.</p>
  <p id="hY0z">Теперь модифицируем приложение WeatherForecast. Помним, что проекты сервисов по нашей задумке должны быть независимыми, поэтому нам нужны копии моделек:</p>
  <figure id="LQYI" class="m_original">
    <img src="https://img2.teletype.in/files/91/7e/917e7359-b177-40d2-85b8-88882cf9e992.png" width="337" />
  </figure>
  <figure id="qWH9" class="m_original">
    <img src="https://img2.teletype.in/files/14/40/1440a46c-44f9-428d-828d-a2994a4de745.png" width="595" />
  </figure>
  <figure id="M16A" class="m_original">
    <img src="https://img2.teletype.in/files/54/6b/546bd6b3-bb45-4045-a32a-36eafe592375.png" width="601" />
  </figure>
  <p id="tyyn">Создадим сервис, который будет отправлять запрос:</p>
  <figure id="RWJj" class="m_original">
    <img src="https://img2.teletype.in/files/13/8b/138bd744-2077-4f33-bba5-c410500ad908.png" width="365" />
  </figure>
  <figure id="rtIj" class="m_original">
    <img src="https://img3.teletype.in/files/6d/2e/6d2e8712-df43-4a27-a465-3f05f078da2b.png" width="568" />
  </figure>
  <figure id="D9mF" class="m_original">
    <img src="https://img2.teletype.in/files/1f/c1/1fc1f13a-09d9-40ce-b6f7-ad38db4532be.png" width="774" />
  </figure>
  <p id="Y5qO">Остановлюсь на паре моментов. </p>
  <p id="nLKM">Первое, timeout запроса должен быть. Практически всегда его выносят в настройки (конфигурационные файлы), чтобы настраивать под конкретные условия (скорость сети, нагрузка). Дефолтные 180 секунд не годятся. Пользователь не будет ждать. Если сервис не критичен для приложения, то есть он не отвечает, тогда &quot;идем&quot; дальше без него. Такие моменты нужно изначально продумывать в архитектуре приложения. </p>
  <p id="e0R7">Второе, обработка ошибок. Ловим, оборачиваем в &quot;свои&quot; специфичные классы ошибок, добавляем контекста, информации, которая потом поможет при диагностике проблем. Это очень сложная тема, которая требует опыта, понимания и интуиции, что, где, как пойдет не так, и какая информация пригодится. </p>
  <p id="L7Q0">Добавляем свойство <em>AdPost </em>в модель страницы и делаем запрос к нашему сервису:</p>
  <figure id="zWQJ" class="m_original">
    <img src="https://img3.teletype.in/files/62/97/62970197-785d-4297-ad2d-540c4c05edfc.png" width="872" />
  </figure>
  <p id="JEgT">Выводим на страницу рекламу:</p>
  <figure id="YlMx" class="m_original">
    <img src="https://img1.teletype.in/files/c4/af/c4af79c8-2766-4c65-88ed-fd9525e2639f.png" width="880" />
  </figure>
  <p id="Q7aX">Нам понадобится добавить в настройки <em>url </em>сервиса <strong>Ads</strong>:</p>
  <figure id="zQco" class="m_original">
    <img src="https://img3.teletype.in/files/a6/56/a656ab20-f669-4177-b27c-2ee81626f31f.png" width="791" />
  </figure>
  <p id="POmK">Настроим контейнер внедрения зависимостей:</p>
  <figure id="Z4q5" class="m_original">
    <img src="https://img2.teletype.in/files/1b/14/1b1456e7-357f-469b-9cbf-5913139f314e.png" width="865" />
  </figure>
  <p id="ls95">Visual Studio позволяет нам во время разработки запускать и отлаживать несколько проектов, для этого нужно открыть свойства Solution:</p>
  <figure id="Zxbg" class="m_original">
    <img src="https://img3.teletype.in/files/63/52/635243d2-d3b0-44ab-9af8-fe5859047dae.png" width="463" />
  </figure>
  <p id="k1Vt">Открываем раздел <em>Common Properties</em> -&gt; <em>Startup Project</em>:</p>
  <figure id="mHoN" class="m_original">
    <img src="https://img4.teletype.in/files/fe/1f/fe1f5f55-5cb2-4c03-be6a-8a4bd8de3c8f.png" width="1058" />
  </figure>
  <p id="zKaL">Выбераем <em>Multiple startup projects</em>, установливаем <em>Action </em>в значение <em>Start </em>для проектов <strong>DeployToProduction.Ads.WebApi</strong> и <strong>DeployToProduction.WeatherForecast.App</strong>, жмем <strong>ОК</strong>:</p>
  <figure id="d9O0" class="m_original">
    <img src="https://img1.teletype.in/files/4a/cd/4acdd54d-d65b-4d03-bde8-a39b247b1867.png" width="1058" />
  </figure>
  <p id="e318">В панели отладки видим:</p>
  <figure id="UhHI" class="m_original">
    <img src="https://img4.teletype.in/files/79/0e/790e06c3-be30-4597-8d22-ebf7cdd78f1a.png" width="512" />
  </figure>
  <p id="xc8j">Помним, что для работы нашего приложения нужны баз данных, поэтому запускаем контейнеры:</p>
  <pre id="NmH9" data-lang="bash">docker run --name weather-forecast-postgres -p 5432:5432 -e POSTGRES_PASSWORD=postgres -d postgres
docker run --name weather-forecast-redis -p 6379:6379 -d redis</pre>
  <p id="huwT">Теперь запускаем наши сервисы:</p>
  <figure id="krvU" class="m_original">
    <img src="https://img4.teletype.in/files/f5/74/f574246c-8556-4dba-a7d6-d07a0e77e523.png" width="1168" />
  </figure>
  <figure id="yJ0E" class="m_original">
    <img src="https://img2.teletype.in/files/5a/37/5a372bd4-e77f-4c83-9765-b3d04a8436f4.png" width="1115" />
  </figure>
  <p id="SVMQ">Проверяем:</p>
  <figure id="LuAm" class="m_original">
    <img src="https://img4.teletype.in/files/bd/86/bd860ff1-8ff7-49d3-80fe-7c403796cbf0.png" width="772" />
  </figure>
  <figure id="AlSW" class="m_original">
    <img src="https://img2.teletype.in/files/d2/67/d26768b3-2510-4328-993b-393e739236ac.png" width="772" />
  </figure>
  <p id="xAnf">Работает!</p>
  <h2 id="Fgbo">З.Ы.</h2>
  <p id="FwaA">Конечно же вы помните, что &quot;тру&quot; разработчики проверяют свой код автоматическими тестами. Пока вы в творческом цикле пишу-пробую инструменты ручного тестирования могут быть вам удобней, но готовый код - это код покрытый авто-тестами. Поэтому к тестированию сервиса Ads мы ещё вернемся. ;) </p>

]]></content:encoded></item><item><guid isPermaLink="true">https://deploy2production.ru/weather-forecast-vm-docker</guid><link>https://deploy2production.ru/weather-forecast-vm-docker?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=deploy2production</link><comments>https://deploy2production.ru/weather-forecast-vm-docker?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=deploy2production#comments</comments><dc:creator>deploy2production</dc:creator><title>WeatherForecast. Виртуальная машина. Docker.</title><pubDate>Wed, 12 Apr 2023 14:36:57 GMT</pubDate><media:content medium="image" url="https://img4.teletype.in/files/79/ba/79ba01e8-1551-4553-98b4-61a2a3777091.png"></media:content><category>Weather Forecast</category><description><![CDATA[<img src="https://img4.teletype.in/files/be/90/be909296-b1d8-4fdf-9f0c-cfffcd9d7ea4.png"></img>Замечательно, когда у вашего любимого провайдера есть готовые образы виртуальных машин с установленными docker-ом и docker compose-ом. Но если нет, то ничего страшного. Docker устанавливается по инструкции с официального сайта (сразу даю ссылочку на вариант установки из репозитория &quot;Install using the apt repository&quot;):]]></description><content:encoded><![CDATA[
  <h2 id="js2H">Предисловие</h2>
  <p id="Pxto">Замечательно, когда у вашего любимого провайдера есть готовые образы виртуальных машин с установленными docker-ом и docker compose-ом. Но если нет, то ничего страшного. Docker устанавливается по инструкции с официального сайта (сразу даю ссылочку на вариант установки из репозитория &quot;Install using the apt repository&quot;):</p>
  <p id="EmLT"><a href="https://docs.docker.com/engine/install/ubuntu/#install-using-the-repository" target="_blank">https://docs.docker.com/engine/install/ubuntu/#install-using-the-repository</a></p>
  <p id="DBSc">Заводите виртуальную машину (описание <a href="https://deploy2production.ru/vm-selectel-test" target="_blank">тут</a>), только выберите конфигурацию &quot;пожирнее&quot;, я для теста взял 2 vCPU, 4RAM, 15 ГБ HDD. Настраивайте подключение по <a href="https://deploy2production.ru/vm-ssh-connect" target="_blank">ssh </a>и устанавливайте docker, опыта копирования команд из инструкции вам должно хватить. </p>
  <p id="y4P7">И так виртуальная машина готова. Docker установлен.</p>
  <h2 id="w4NK">Поехали</h2>
  <p id="6B0m">Начнем с решения проблем. Если вы успешно установили docker под root-ом и потом создали своего пользователя, настроили ssh и подключились к виртуальной машине (как это сделал я), то использованием docker-а возникнут проблемы:</p>
  <figure id="Ocxk" class="m_original">
    <img src="https://img4.teletype.in/files/be/90/be909296-b1d8-4fdf-9f0c-cfffcd9d7ea4.png" width="1115" />
  </figure>
  <p id="oWUm">Для решения нам нужно добавить своего пользователя в группу &quot;docker&quot;. Кстати посмотреть список всех групп в системе можно командой:</p>
  <pre id="3vsh" data-lang="bash"> cat /etc/group</pre>
  <p id="JDOW">А проверить список групп, в которые входит ваш пользователь:</p>
  <pre id="RiHs" data-lang="bash">groups</pre>
  <p id="sWnZ">Добавляемся в группу &quot;docker&quot; командой:</p>
  <pre id="lhUz" data-lang="bash">sudo usermod -aG docker ${USER}</pre>
  <p id="ucFR">Для того, чтобы изменения применились, нужно разлогиниться и залогиниться, после проверяем:</p>
  <figure id="j23Y" class="m_original">
    <img src="https://img1.teletype.in/files/cd/cb/cdcbcd7b-2cc0-4de4-aef8-c04655d631bb.png" width="661" />
  </figure>
  <figure id="UWYK" class="m_original">
    <img src="https://img4.teletype.in/files/31/c5/31c57691-92ca-483a-926e-ae4028ba33b7.png" width="1079" />
  </figure>
  <p id="WOWM">Работает. Теперь логинимся в наш Container Registry командой:</p>
  <pre id="sLp7" data-lang="bash">docker login cr.selcloud.ru</pre>
  <p id="ovS4">Для проверки скачаем образ нашего приложения и утилиты:</p>
  <pre id="s6NG" data-lang="bash">docker pull cr.selcloud.ru/deploy2production/weather-forecast-app-img:v2.0</pre>
  <pre id="8JNW" data-lang="bash">docker pull cr.selcloud.ru/deploy2production/weather-forecast-setup-img:v2.0</pre>
  <figure id="78Ju" class="m_original">
    <img src="https://img1.teletype.in/files/48/7f/487ff3dd-b68a-4ef5-8f50-9b15ad9cb33c.png" width="1079" />
  </figure>
  <h3 id="UWld">УПС</h3>
  <p id="A2bo">Посыплю голову пеплом. В пролом <a href="https://deploy2production.ru/weather-forecast-container-registry" target="_blank">посте</a> я накосячил с копированием команд, и собрал один и тот же образ под разными именами (хотя в тексте поста все корректно), и &quot;залил&quot; так в Container Registry. Будь я зорким как орел, я бы заметил одинаковый хэш образов. Но нет. Поэтому я пересобрал опубликовал образы с новым тегом-версией v2.0. Учимся на ошибках - дважды проверяем, хэши нам в помощь.</p>
  <figure id="Aixl" class="m_original">
    <img src="https://img1.teletype.in/files/84/37/843766c1-1e35-461d-bb17-10446850e6dd.png" width="1116" />
  </figure>
  <p id="rds9">Продолжим. Давайте для разминки запустим наше приложение в &quot;dev&quot; варианте, как мы это делаем локально, с базами данных в контейнерах. </p>
  <pre id="TnGj" data-lang="bash"> mkdir dev
 cd dev
 nano ./docker-compose.yml</pre>
  <p id="Ywqr">Нам понадобится docker-compose файл с некоторыми изменениями:</p>
  <pre id="UBl6" data-lang="yaml">name: &quot;weather-forecast&quot;

networks:
  weather-forecast-net:

services:
  postgres:
    image: &quot;postgres&quot;
    container_name: &quot;weather-forecast-postgres&quot;
    networks:
      - weather-forecast-net
    ports:
      - &quot;5432:5432&quot;
    environment:
      POSTGRES_PASSWORD: postgres
  redis:
    image: &quot;redis&quot;
    container_name: &quot;weather-forecast-redis&quot;
    networks:
      - weather-forecast-net
    ports:
      - &quot;6379:6379&quot;
  app:
    image: &quot;cr.selcloud.ru/deploy2production/weather-forecast-app-img:v2.0&quot;
    container_name: &quot;weather-forecast-app&quot;
    networks:
      - weather-forecast-net
    ports:
      - &quot;80:80&quot;
    environment:
       ASPNETCORE_ENVIRONMENT: Development
       DOTNET_PRINT_TELEMETRY_MESSAGE: false
       ConnectionStrings__Postgres: Host=weather-forecast-postgres;User Id=postgres;Password=postgres;Database=postgres
       ConnectionStrings__Redis: weather-forecast-redis:6379
    depends_on:
      postgres:
        condition: service_started
      redis:
        condition: service_started</pre>
  <p id="8OlM">Теперь сервис app использует образ из Container Registry вместо сборки из docker-файл. Ещё мы используем порт хота 80.</p>
  <p id="jrMU">Поднимаем наши контейнеры:</p>
  <pre id="qRb6" data-lang="bash">docker compose up -d</pre>
  <p id="nBdg">Без удобного клиента Docker Desktop проверить какие контейнеры запущены можно командой:</p>
  <pre id="JoNq" data-lang="bash">docker ps</pre>
  <figure id="5MrB" class="m_original">
    <img src="https://img2.teletype.in/files/df/a3/dfa3580b-23aa-4058-9a05-55ff78f2a25b.png" width="2300" />
  </figure>
  <p id="hwu8">Фаервол на виртуальной машине мы ещё не настраивали, поэтому мы должны увидеть наш сайт снаружи:</p>
  <figure id="dAMT" class="m_original">
    <img src="https://img3.teletype.in/files/ee/01/ee0113a2-1cec-4f07-9567-557a8fa93396.png" width="772" />
  </figure>
  <p id="BNni">И убедившись в том, что всё работает, останавливаем и удаляем контейнеры:</p>
  <pre id="1iuK" data-lang="bash">docker compose down</pre>
  <p id="GpAC">Теперь попробуем запустить наше приложение с базами данных от Selectel. Как создать базы данных в Selectel, вы уже <a href="https://deploy2production.ru/weather-forecast-dev-deploy-selectel" target="_blank">знаете</a>. Я прошлые базы данных удалил, поэтому создаю новые пользуясь своей же инструкцией, удобно =)</p>
  <figure id="lKw3" class="m_original">
    <img src="https://img1.teletype.in/files/49/1f/491f10d5-8103-488a-9acf-76130100195e.png" width="816" />
  </figure>
  <pre id="OvGS" data-lang="bash">cd ~
mkdir dev-db
cd dev-db
nano ./docker-compose.yml</pre>
  <p id="1CQd">Содержимое файла <em>docker-compose.yml</em>:</p>
  <pre id="cYmR" data-lang="yaml">name: &quot;weather-forecast&quot;

networks:
  weather-forecast-net:

services:
  app:
    image: &quot;cr.selcloud.ru/deploy2production/weather-forecast-app-img:v2.0&quot;
    container_name: &quot;weather-forecast-app&quot;
    networks:
      - weather-forecast-net
    ports:
      - &quot;80:80&quot;
    environment:
       ASPNETCORE_ENVIRONMENT: Development
       DOTNET_PRINT_TELEMETRY_MESSAGE: false
       ConnectionStrings__Postgres: Host=192.168.0.107;User Id=delilah;Password=*******;Database=weatherforecast
       ConnectionStrings__Redis: 192.168.0.222:6379,password==*********</pre>
  <p id="ClFH">В этот раз запустим приложение в интерактивном режиме, чтобы увидеть лог:</p>
  <pre id="qETb" data-lang="bash">docker compose up</pre>
  <figure id="xELU" class="m_original">
    <img src="https://img1.teletype.in/files/05/8a/058a6d65-e613-44e0-9e9b-1ebd291eed9d.png" width="1115" />
  </figure>
  <p id="anXw">И мы видим, что приложение успешно запускается и работает, но происходит ошибка при выполнении второго sql-скрипта, который настраивает схему базы данных, создает пользователя для приложения. Вот такой вот сюрприз, пользователь, которого мы создали через интерфейс личного кабинет (а это единственный доступный вариант), просто не имеет необходимых прав. Такое вот ограничение облачной базы данных Postgres в Selectel, связано это с реализацией на стороне Selectel. Но не унываем, значит нам нужно будет учесть этот момент и внести изменения в свой код ;)</p>
  <p id="9Vij">Останавливаем приложение, Ctrl+C. Удаляем:</p>
  <pre id="LzW6" data-lang="bash">docker compose down</pre>
  <p id="dMoL">Давайте, для полноты картины создадим compose файлы для приложения и утилиты настройки базы данных так, как бы мы делали в &quot;бою&quot;.</p>
  <pre id="CFzh" data-lang="bash">cd ~
mkdir prod
cd prod
mkdir setup
cd setup
nano ./docker-compose.yml</pre>
  <pre id="1ReQ" data-lang="yaml">services:
  setup:
    image: &quot;cr.selcloud.ru/deploy2production/weather-forecast-setup-img:v2.0&quot;
    container_name: &quot;weather-forecast-setup&quot;
    environment:
       ConnectionStrings__Postgres: Host=192.168.0.107;User Id=delilah;Password=pwd;Database=weatherforecast
</pre>
  <pre id="OVPv" data-lang="bash">docker compose up</pre>
  <figure id="7cYG" class="m_original">
    <img src="https://img2.teletype.in/files/df/1b/df1bab37-8387-4ecb-8d2e-a2021030927a.png" width="1115" />
  </figure>
  <pre id="0cGk" data-lang="bash">cd ~/prod
mkdir app
cd app
nano ./docker-compose.yml</pre>
  <pre id="pmYn" data-lang="bash">name: &quot;weather-forecast&quot;

networks:
  weather-forecast-net:

services:
  app:
    image: &quot;cr.selcloud.ru/deploy2production/weather-forecast-app-img:v2.0&quot;
    container_name: &quot;weather-forecast-app&quot;
    networks:
      - weather-forecast-net
    ports:
      - &quot;80:80&quot;
    environment:
       ASPNETCORE_ENVIRONMENT: Production
       DOTNET_PRINT_TELEMETRY_MESSAGE: false
       ConnectionStrings__Postgres: Host=192.168.0.107;User Id=delilah;Password=pwd;Database=weatherforecast
       ConnectionStrings__Redis: 192.168.0.222:6379,password==pwd</pre>
  <pre id="gocV" data-lang="bash">docker compose up -d</pre>
  <pre id="Eo3v" data-lang="bash">docker compose ps</pre>
  <figure id="ZqDf" class="m_original">
    <img src="https://img1.teletype.in/files/45/17/45175cac-6616-4ac9-bf53-987214988401.png" width="1115" />
  </figure>
  <figure id="ZhGV" class="m_original">
    <img src="https://img2.teletype.in/files/11/ef/11efb97f-ce8c-423f-8733-a0c34cce783b.png" width="772" />
  </figure>
  <p id="8kvk">Подведем итоги. Мы разобрались на базовом уровне как разрабатывать, упаковывать и распространять приложение с помощью docker-а, и запускать его потом на виртуальной машине. Теперь дальше мы будем двигаться глубже =)</p>

]]></content:encoded></item><item><guid isPermaLink="true">https://deploy2production.ru/weather-forecast-container-registry</guid><link>https://deploy2production.ru/weather-forecast-container-registry?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=deploy2production</link><comments>https://deploy2production.ru/weather-forecast-container-registry?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=deploy2production#comments</comments><dc:creator>deploy2production</dc:creator><title>WeatherForecast. Container Registry.</title><pubDate>Sun, 02 Apr 2023 11:21:54 GMT</pubDate><media:content medium="image" url="https://img4.teletype.in/files/ba/68/ba6874ba-6d76-4748-b93a-d06587e89bb0.png"></media:content><category>Weather Forecast</category><description><![CDATA[<img src="https://img3.teletype.in/files/6d/4b/6d4b61da-5c2d-4bdc-9842-8076aec9f63e.png"></img>И так, мы с вами разобрались как &quot;заворачивать&quot; приложение в docker, как организовывать удобную работу с контейнерами с помощью docker compose (раз, два). Теперь пришло время попробовать эти знания в запуске приложения на виртуальном сервере.]]></description><content:encoded><![CDATA[
  <h2 id="uGJr">Предисловие</h2>
  <p id="bF94">И так, мы с вами разобрались как &quot;заворачивать&quot; приложение в <a href="https://deploy2production.ru/weather-forecast-docker" target="_blank">docker</a>, как организовывать удобную работу с контейнерами с помощью docker compose (<a href="https://deploy2production.ru/weather-forecast-docker-compose" target="_blank">раз</a>, <a href="https://deploy2production.ru/weather-forecast-docker-compose-setup" target="_blank">два</a>). Теперь пришло время попробовать эти знания в запуске приложения на виртуальном сервере.</p>
  <h2 id="6YZS">Поехали</h2>
  <p id="kAl5">Исходники к этому посту такие же как на предыдущем <a href="https://github.com/deploy2production/weather-forecast/tree/setup" target="_blank">шаге</a>.</p>
  <p id="smPc">Начнем с чистого листа, запустим Docker Desktop и удалим ранее собранные образы нашего приложения.</p>
  <p id="0hCg">Собираем docker-образ приложения:</p>
  <pre id="eCpi" data-lang="shell">docker build -t weather-forecast-app-img -f Dockerfile.app .</pre>
  <p id="s8PX">Собираем docker-образ утилиты <strong>setup</strong>:</p>
  <pre id="Ac6T" data-lang="shell">docker build -t weather-forecast-setup-img -f Dockerfile.setup .</pre>
  <figure id="8KOE" class="m_original">
    <img src="https://img3.teletype.in/files/6d/4b/6d4b61da-5c2d-4bdc-9842-8076aec9f63e.png" width="1116" />
  </figure>
  <p id="xoGx">Образы собраны, но они находятся на диске нашего компьютера. Как же они попадут на виртуальный сервер, чтобы мы могли уже на нем запустить контейнеры из этих образов?</p>
  <p id="S8Rr">Хранилище для образов называется Container Registry.</p>
  <p id="6zCE">Когда мы запускали контейнеры баз данных или собирали образ для нашего приложения, <strong>docker </strong>скачивал на наш диск базовые образы из хранилища <a href="https://hub.docker.com/" target="_blank">Docker Hub</a>. Docker Hub предоставляет какой-то объем для бесплатного размещения публичных образов и платные услуги для приватного использования. Но нам будет удобнее и дешевле создать Container Registry в Selectel.</p>
  <p id="Pk2Z">Открываем панель управления Selectel, выбираем <em>Облачные платформы</em> -&gt; <em>Container Registry</em>:</p>
  <figure id="ne5o" class="m_original">
    <img src="https://img1.teletype.in/files/04/ac/04ac05ea-2699-4e89-acee-c65e5072c4e3.png" width="1213" />
  </figure>
  <p id="jAkl">Жмем &quot;<strong><em>Начать работу</em></strong>&quot;, вводим имя реестра и жмем &quot;<strong><em>Создать</em></strong>&quot;:</p>
  <figure id="1ZAf" class="m_original">
    <img src="https://img1.teletype.in/files/c3/c8/c3c86823-4814-4a65-90c4-80fbddc86a52.png" width="544" />
  </figure>
  <p id="B8BE">Видим с писке созданное хранилище, кликаем по нему:</p>
  <figure id="NDYL" class="m_original">
    <img src="https://img3.teletype.in/files/2d/a9/2da917ea-a8ab-4c32-949d-b4343e673538.png" width="810" />
  </figure>
  <p id="etmy">Жмем на кнопку с тремя точками, выбираем пункт &quot;Получить токен&quot;:</p>
  <figure id="xG6u" class="m_original">
    <img src="https://img3.teletype.in/files/24/e7/24e74943-268c-4b06-b9ef-a7d9376f7221.png" width="811" />
  </figure>
  <p id="7VMS">Сохраняем к себе в надежное место логи и пароль:</p>
  <figure id="HhlY" class="m_original">
    <img src="https://img2.teletype.in/files/db/e4/dbe46b41-021f-45e6-ae17-519a6e89a9e9.png" width="607" />
  </figure>
  <p id="knCe">Открываем терминал и логинимся в наш Container Registry:</p>
  <pre id="X5tA" data-lang="shell">docker login cr.selcloud.ru</pre>
  <figure id="DfGy" class="m_original">
    <img src="https://img2.teletype.in/files/9d/f9/9df9f326-2cdc-44b1-b0db-e5e389be0a1a.png" width="970" />
  </figure>
  <p id="MyUS">Теперь нужно присвоить образу тэг:</p>
  <pre id="Fby9" data-lang="shell">docker tag weather-forecast-app-img cr.selcloud.ru/deploy2production/weather-forecast-app-img:v1.0</pre>
  <figure id="pvwV" class="m_original">
    <img src="https://img3.teletype.in/files/63/dc/63dc1e1e-89b1-4d69-ad28-e3441733b10a.png" width="1116" />
  </figure>
  <p id="C0rI">И загрузить образ в Container Registry:</p>
  <pre id="vURh" data-lang="shell">docker push cr.selcloud.ru/deploy2production/weather-forecast-app-img:v1.0</pre>
  <figure id="c2sE" class="m_original">
    <img src="https://img2.teletype.in/files/1b/a3/1ba327fd-35c8-4c6b-8de5-1c3161d2edb2.png" width="1115" />
  </figure>
  <figure id="7fN6" class="m_original">
    <img src="https://img1.teletype.in/files/47/7d/477dc8a0-f00b-4e71-8115-52a384b5b5c1.png" width="814" />
  </figure>
  <p id="CssG">Аналогично для утилиты setup:</p>
  <pre id="xGs6" data-lang="shell">docker tag weather-forecast-setup-img cr.selcloud.ru/deploy2production/weather-forecast-setup-img:v1.0</pre>
  <pre id="Ojuw" data-lang="shell">docker push cr.selcloud.ru/deploy2production/weather-forecast-setup-img:v1.0</pre>
  <p id="Eia1">И так, образы загружены:</p>
  <figure id="8iaS" class="m_original">
    <img src="https://img2.teletype.in/files/11/b0/11b0c71a-c673-4940-95ec-5ac6f34b7584.png" width="806" />
  </figure>
  <p id="9vEF">Следующий шаг - настройка виртуальной машины.</p>

]]></content:encoded></item><item><guid isPermaLink="true">https://deploy2production.ru/weather-forecast-docker-compose-setup</guid><link>https://deploy2production.ru/weather-forecast-docker-compose-setup?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=deploy2production</link><comments>https://deploy2production.ru/weather-forecast-docker-compose-setup?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=deploy2production#comments</comments><dc:creator>deploy2production</dc:creator><title>WeatherForecast. Setup.</title><pubDate>Sat, 01 Apr 2023 23:27:33 GMT</pubDate><media:content medium="image" url="https://img4.teletype.in/files/37/28/37285805-de57-42c7-9fff-0691363ff64d.png"></media:content><category>Weather Forecast</category><description><![CDATA[<img src="https://img4.teletype.in/files/b1/f8/b1f88925-370a-4589-ae89-b6b126b2a9ce.png"></img>Мы начали разбираться с Docker Compose на предыдущем шаге. Теперь хочется охватить чуть больше полезных фич этого инструмента, которые сильно помогут в разработке. Реальные проекты состоят нескольких приложений, программ, сервисов. В нашем случае мы рассмотрим следующий момент. Сервис при запуске ожидает настроенную схему базы данных Postgres. В режиме разработки мы при старте выполняли скрипты sql для создании таблиц. Но в &quot;бою&quot; такой подход не подойдет, так как у нас может одновременно запускаться несколько копий приложения или само приложение может состоять из нескольких сервисов. Обычно процедуру миграции (обновления) схемы базы данных выносят в отдельное приложение или используют для этого сторонний инструмент. На пример, если...]]></description><content:encoded><![CDATA[
  <h2 id="GhRT">Предисловие</h2>
  <p id="rwip">Мы начали разбираться с Docker Compose на предыдущем <a href="https://deploy2production.ru/weather-forecast-docker-compose" target="_blank">шаге</a>. Теперь хочется охватить чуть больше полезных фич этого инструмента, которые сильно помогут в разработке. Реальные проекты состоят нескольких приложений, программ, сервисов. В нашем случае мы рассмотрим следующий момент. Сервис при запуске ожидает настроенную схему базы данных Postgres. В режиме разработки мы при старте выполняли скрипты sql для создании таблиц. Но в &quot;бою&quot; такой подход не подойдет, так как у нас может одновременно запускаться несколько копий приложения или само приложение может состоять из нескольких сервисов.  Обычно процедуру миграции (обновления) схемы базы данных выносят в отдельное приложение или используют для этого сторонний инструмент. На пример, если вы используете Entity Framework, то выполняете команду &#x27;<em>dotnet ef database update</em>&#x27;. Мы создадим консольную программу, которая будет выполнять обновление схемы базы данных.</p>
  <h2 id="thb0">Поехали</h2>
  <p id="QNVN">Исходники к этому посту находятся <a href="https://github.com/deploy2production/weather-forecast/tree/setup" target="_blank">тут</a>.</p>
  <p id="AGM4">Создадим консольный проект, добавим необходимые зависимости:</p>
  <figure id="sD6a" class="m_original">
    <img src="https://img4.teletype.in/files/b1/f8/b1f88925-370a-4589-ae89-b6b126b2a9ce.png" width="458" />
  </figure>
  <p id="p2Iy">Добавим кофигурационные файлы и код:</p>
  <figure id="QYrS" class="m_original">
    <img src="https://img3.teletype.in/files/6c/5f/6c5f9d0a-1625-4acb-9ff7-4cccf0f76606.png" width="367" />
  </figure>
  <figure id="qaql" class="m_original">
    <img src="https://img3.teletype.in/files/29/ab/29ab49af-c956-4df2-9493-5ed8722f79e0.png" width="775" />
  </figure>
  <figure id="OpSG" class="m_original">
    <img src="https://img4.teletype.in/files/3e/c2/3ec29872-d438-44a5-a47f-ca681f2f4b9b.png" width="703" />
  </figure>
  <p id="gTsC">Для демонстрации тут всё максимально просто: считываем настройки подключения, вызываем уже знакомый метод <em>Setup </em>класса <em>Db</em>.</p>
  <p id="Ij8t">Позвольте уделить внимание ещё одному важному моменту. Ранее наше приложение подключалось к postgres пользователем с &quot;админскими&quot; правами. Что не есть хорошо и не безопасно. Приложение должно иметь минимально необходимый уровень доступа к базе данных. Давайте это исправим. В проект &quot;<strong><em>DeployToProduction.WeatherForecast.Data.Psql</em></strong>&quot; нужно добавить новый sql-скрипт, который будет создавать пользователя для нашего приложения:</p>
  <figure id="MAgc" class="m_original">
    <img src="https://img1.teletype.in/files/86/bc/86bc4fc8-ddc9-4868-8379-6e75a6354d09.png" width="455" />
  </figure>
  <figure id="T6qr" class="m_original">
    <img src="https://img1.teletype.in/files/88/da/88da4ebb-2230-4bdf-8bb4-e51572c7a4eb.png" width="543" />
  </figure>
  <p id="vJh8">Не забудьте в настройках нового скрипта установить свойство &quot;<em>Build Action</em>&quot;:</p>
  <figure id="is1P" class="m_original">
    <img src="https://img4.teletype.in/files/f4/f1/f4f1c207-1551-4cee-b645-a8f11509b606.png" width="393" />
  </figure>
  <p id="Ye2n">И, конечно же, нужно проверить как наш код работает с новым пользователем с помощью автоматического теста, модифицируем уже имеющийся тест:</p>
  <figure id="FIuj" class="m_original">
    <img src="https://img1.teletype.in/files/41/ba/41ba0985-d274-44cf-a313-16c4563f6e40.png" width="796" />
  </figure>
  <p id="C3qx">В 31-ой строке я меняю логин/пароль в строке подключения на нового пользователя.</p>
  <p id="y5DS">В коде самого приложения мы убираем обновление схемы базы данных под условие с проверкой среды выполнения, таким образом при разработке нам будет удобно - схема обновляется, а в &quot;бою&quot; приложение будет ожидать подготовленной базы данных:</p>
  <figure id="ZqTY" class="m_original">
    <img src="https://img2.teletype.in/files/1d/33/1d33d963-0bf4-4db4-9228-1480ad845d22.png" width="854" />
  </figure>
  <p id="9otD">Перейдем к docker-у. </p>
  <p id="iYgS">Удалим ранее созданные образы контейнера нашего приложения. </p>
  <p id="T11L">У нас теперь будет два docker-файла, один &quot;<em>Dockerfile.app</em>&quot; для сборки приложения, второй &quot;<em>Dockerfile.setup</em>&quot; для сборки нашей утилиты setup:</p>
  <figure id="iRJr" class="m_original">
    <img src="https://img1.teletype.in/files/89/7e/897ee6c7-e617-4f04-95fb-5b2ae5cad936.png" width="779" />
  </figure>
  <figure id="NXdq" class="m_original">
    <img src="https://img4.teletype.in/files/f6/6f/f66fc194-b0d5-4631-967c-15e1badde47f.png" width="873" />
  </figure>
  <p id="Z10I">Обратите внимание, что при сборке консольного проекта на linux, расширение файла будет &quot;dll&quot;, и его также нужно запускать через dotnet.</p>
  <p id="pqFu">Веб приложение использует Postgres и Redis, для консольной утилиты setup нужен только Postgres, но нам лень (ладно, мне лень) и не хочется усложнять пример, поэтому мы создадим один файл docker compose &quot;<em>docker-compose.db.yml</em>&quot; для запуска баз данных, который далее будем переиспользовать:</p>
  <figure id="UCmo" class="m_original">
    <img src="https://img4.teletype.in/files/7b/2d/7b2de4a4-21d1-4c07-83d0-6b3fe31f7528.png" width="467" />
  </figure>
  <p id="p5JK">Добавим &quot;<em>docker-compose.setup.yml</em>&quot; для запуска setup:</p>
  <figure id="0Q8u" class="m_original">
    <img src="https://img1.teletype.in/files/c0/bb/c0bb3c8f-735b-498a-a2e8-8fdd5cedb43d.png" width="1029" />
  </figure>
  <p id="XOei">Запускаем setup в контейнере командой:</p>
  <pre id="bscJ" data-lang="bash">docker compose -f docker-compose.db.yml -f docker-compose.setup.yml up</pre>
  <p id="YNVZ">Обратите внимание, я указываю оба файла.</p>
  <p id="TNIw">В выводе в консоль мы видим, что два наших sql-скрипты были выполнены:</p>
  <figure id="LLkU" class="m_original">
    <img src="https://img4.teletype.in/files/f8/63/f863e6b9-1fd4-48f3-a485-5caf52e56ff4.png" width="1115" />
  </figure>
  <p id="2isM">Ctrl+C - остановить, удалить остановленные контейнеры:</p>
  <pre id="hcc7" data-lang="bash">docker compose -f docker-compose.db.yml -f docker-compose.setup.yml down</pre>
  <p id="OQ06">Теперь docker compose для приложения. У нас будет два файла, один &quot;docker-compose.app.dev.yml&quot; для запуска при разработке, второй &quot;docker-compose.app.prd.yml&quot; - &quot;боевой&quot;:</p>
  <figure id="CpKf" class="m_original">
    <img src="https://img4.teletype.in/files/ba/73/ba735c12-3b0a-4e1c-b700-2ba0e343530f.png" width="1023" />
  </figure>
  <p id="9VXs">Тут у нас и <strong><em>Environment=Development</em></strong> и логин в Postgres админский. А, вот, второй будет интереснее:</p>
  <figure id="u0Th" class="m_original">
    <img src="https://img1.teletype.in/files/ce/31/ce31b4ec-4a33-4824-a784-be5332141de4.png" width="1025" />
  </figure>
  <p id="lZ8G">Мы добавили в <strong>services </strong>нашу утилиту <strong>setup</strong>, которая будет выполнена перед запуском <strong>app</strong>, так как <strong>app </strong>зависит от <strong>setup</strong>, смотри <strong>depends_on</strong>. И логин/пароль в строке подключения Postgres используется пользователь, которого мы создали в новом sql-скрипте.</p>
  <p id="hhV4">Запустим &quot;боевой&quot; compose:</p>
  <pre id="rln6" data-lang="bash">docker compose -f docker-compose.db.yml -f docker-compose.app.prd.yml up</pre>
  <figure id="YZte" class="m_original">
    <img src="https://img4.teletype.in/files/77/d4/77d4896a-e37c-45db-aa58-62c1a2c5e384.png" width="1115" />
  </figure>
  <p id="6vFp">Запустилось. Видим работающие контейнеры баз данных и приложения, а контейнер setup выполнил свое дело и остановился:</p>
  <figure id="GbY2" class="m_original">
    <img src="https://img1.teletype.in/files/88/e0/88e0f540-84c9-4492-b736-df3ca9f22ef5.png" width="849" />
  </figure>
  <p id="Jm3B">Проверяем в браузере:</p>
  <figure id="3NYo" class="m_original">
    <img src="https://img1.teletype.in/files/40/1b/401b0d42-52cb-46a1-a0a4-b21fdac889cb.png" width="772" />
  </figure>
  <p id="9tfK">Работает!</p>
  <p id="7dCI">Ctrl+C - остановить. Удалить контейнеры:</p>
  <pre id="hTiu" data-lang="bash">docker compose -f docker-compose.db.yml -f docker-compose.app.prd.yml down</pre>
  <p id="1t3V">Таким образом вы можете упростить себе процесс разработки, создавая переиспользуемые части инфраструктуры вашего приложения или организуя специфичные режимы работы. </p>
  <h2 id="A7Ec">З.Ы.</h2>
  <p id="scs0">Ну, вы уже догадались... нет нам покоя пока тесты не будут полностью автоматизированы, поэтому вот код UI теста, который запускает все базы данных и приложение в контейнерах и &quot;прокликивает&quot; с помощью Selenium:</p>
  <figure id="pMmm" class="m_original">
    <img src="https://img1.teletype.in/files/4c/a0/4ca0dafa-7f58-4346-a3b6-bfd5163c3503.png" width="960" />
  </figure>

]]></content:encoded></item><item><guid isPermaLink="true">https://deploy2production.ru/weather-forecast-docker-compose</guid><link>https://deploy2production.ru/weather-forecast-docker-compose?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=deploy2production</link><comments>https://deploy2production.ru/weather-forecast-docker-compose?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=deploy2production#comments</comments><dc:creator>deploy2production</dc:creator><title>WeatherForecast. Docker Compose.</title><pubDate>Sun, 19 Mar 2023 11:47:42 GMT</pubDate><media:content medium="image" url="https://img3.teletype.in/files/6f/1e/6f1eb009-2728-4794-a13a-a1cb4041a0b0.png"></media:content><description><![CDATA[<img src="https://img3.teletype.in/files/28/40/28406c00-c019-42d5-87f7-e73c4c6612b5.png"></img>На предыдущих шагах (dev, Docker-изация) мы запускали контейнеры баз данных руками. Да, это полезно для понимания и тренировки, но не удобно в повседневной работе. Для сборки необходимого окружения одной командой есть Docker Compose - один файл описывает всё необходимое. Попробуем запустить всё наше приложение одни легким движением руки.]]></description><content:encoded><![CDATA[
  <h2 id="wFpV">Предисловие</h2>
  <p id="D9ZP">На предыдущих шагах (<a href="https://deploy2production.ru/weather-forecast-dev" target="_blank">dev</a>, <a href="https://deploy2production.ru/weather-forecast-docker" target="_blank">Docker-изация</a>) мы запускали контейнеры баз данных руками. Да, это полезно для понимания и тренировки, но не удобно в повседневной работе. Для сборки необходимого окружения одной командой есть Docker Compose - один файл описывает всё необходимое. Попробуем запустить всё наше приложение одни легким движением руки.</p>
  <h2 id="Sdhf">Поехали</h2>
  <p id="LGx3">Исходники к этому посту находятся <a href="https://github.com/deploy2production/weather-forecast/tree/docker-compose" target="_blank">тут</a>.</p>
  <p id="kKos">Как всегда сначала почистим запущенные контейнеры и ранее созданные образы.</p>
  <p id="gmTI">Добавим в проект файл <strong><em>docker-compose.yml</em></strong>:</p>
  <figure id="e8Ui" class="m_original">
    <img src="https://img3.teletype.in/files/28/40/28406c00-c019-42d5-87f7-e73c4c6612b5.png" width="450" />
  </figure>
  <pre id="VFQX" data-lang="yaml">version: &quot;3.9&quot;

name: &quot;weather-forecast&quot;

networks:
  weather-forecast-net:

services:
  postgres:
    image: &quot;postgres&quot;
    container_name: &quot;weather-forecast-postgres&quot;
    networks:
      - weather-forecast-net
    ports:
      - &quot;5432:5432&quot;
    environment:
      POSTGRES_PASSWORD: postgres
  redis:
    image: &quot;redis&quot;
    container_name: &quot;weather-forecast-redis&quot;
    networks:
      - weather-forecast-net
    ports:
      - &quot;6379:6379&quot;
  app:
    container_name: &quot;weather-forecast-app&quot;
    image: &quot;weather-forecast-app-img&quot;
    build:
      context: .
      dockerfile: Dockerfile
    networks:
      - weather-forecast-net
    ports:
      - &quot;5022:80&quot;
    environment:
       ASPNETCORE_ENVIRONMENT: Production
       DOTNET_PRINT_TELEMETRY_MESSAGE: false
       ConnectionStrings__Postgres: Host=weather-forecast-postgres;Username=postgres;Password=postgres;Database=postgres
       ConnectionStrings__Redis: weather-forecast-redis:6379
    depends_on:
      postgres:
        condition: service_started
      redis:
        condition: service_started</pre>
  <p id="PGwp">Пройдемся по этому файлу. </p>
  <p id="r4x0"><strong>networks </strong>- определяем сеть в которой будут работать наши контейнеры. </p>
  <p id="EW4k"><strong>services </strong>- перечисляем нужные нам контейнеры. </p>
  <p id="lbSP"><strong>postgres </strong>- указываем образ, имя контейнера, сеть, к которой его подключить, порты и переменные окружения. </p>
  <p id="y51i">Аналогично <strong>redis</strong>. </p>
  <p id="YMBg"><strong>app </strong>- наше приложение, его контейнер будет собираться из <strong><em>Dockerfile</em></strong>-а, также имя, сеть, порты и переменные окружения мы уже выносим сюда, ещё нужно указать зависимости, чтобы наше приложение запускалось после того, как базы данных будут готовы.</p>
  <p id="7bZ6">Подредактируем <strong><em>Dockerfile</em></strong>, нам уже не нужно указывать в нем переменные окружения:</p>
  <pre id="Z9W3" data-lang="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 .

# Start App
ENTRYPOINT [&quot;dotnet&quot;, &quot;DeployToProduction.WeatherForecast.App.dll&quot;]</pre>
  <p id="NueE">Дл запуска открываем терминал в каталоге проекта и выполняем команду:</p>
  <pre id="rbCz" data-lang="bash">docker compose up</pre>
  <figure id="ztmG" class="m_original">
    <img src="https://img4.teletype.in/files/3d/6d/3d6d32b0-6e16-42df-9258-9121d806b73f.png" width="948" />
  </figure>
  <p id="1vu4">Команда &quot;<em>docker compose up</em>&quot; запускает все контейнеры перечисленные в файле <strong><em>docker-compose.yml</em></strong> и выводит в консоль их логи. Чтобы остановить контейнеры нажмите <strong>Ctrl+C</strong>. Для запуска в фоновом режиме используйте параметр <strong>-d</strong>:</p>
  <pre id="wqKJ" data-lang="bash">docker compose up -d</pre>
  <p id="VSlV">Удалить все контейнеры можно командой:</p>
  <pre id="YX8e" data-lang="bash">docker compose down</pre>
  <p id="oOgr">Если вы внесли изменения в проект или <strong><em>Dockerfile</em></strong>, то пересобрать образ можно командой</p>
  <pre id="dS2h" data-lang="bash">docker compose build</pre>
  <p id="9XTl">Есть маленький нюанс. Наше приложение при старте выполняет обновление схемы базы данных Postgres. Хоть мы и указали, что контейнер приложения зависит от контейнера postgres, в момент выполнения запуска приложения база данных postgres может быть ещё не готова принимать запросы. Способов решения этой проблемы несколько, один из них - воспользоваться фичей docker compose - <strong>healthcheck</strong>. Но, так как я не хочу сильно закапывать вас в эти тонкости (при том что первый результат поиска из гугла и SO не сработает), я сделаю более простую и понятную проверку в коде <strong><em>Db.cs</em></strong>:</p>
  <figure id="lOAL" class="m_original">
    <img src="https://img4.teletype.in/files/b6/8e/b68e3d00-df76-436a-94bf-e8ff8ae1ff10.png" width="835" />
  </figure>
  <p id="h6VE">И так, проверяем:</p>
  <figure id="6tip" class="m_original">
    <img src="https://img1.teletype.in/files/42/c4/42c4fad8-c3bc-4c69-95d4-c0f564053902.png" width="799" />
  </figure>
  <figure id="Q6C3" class="m_original">
    <img src="https://img3.teletype.in/files/e6/f8/e6f8b298-c6d4-435e-83de-a1173339bcc9.png" width="793" />
  </figure>
  <p id="HgDM">Работает!</p>
  <p id="DgbW">Пару слов в сторону. Мы ручками создавали простейший проект и настраивали работу с docker, чтобы вся эта кухня не казалась магией. Сегодня Visual Studio из коробки поддерживает интегрированную работу с Docker. Вы просто создаете новый проект и ставити галочку &quot;<em>Enable Docker</em>&quot;. VS создаст для вас Dockerfile и настроит запуск и отладку приложения в контейнере. Более того, вы просто сможете <a href="https://learn.microsoft.com/ru-ru/visualstudio/containers/tutorial-multicontainer?view=vs-2022" target="_blank">добавить</a> <strong><em>docker-compose.yml</em></strong> для инфраструктуры приложения.</p>
  <figure id="zVaB" class="m_original">
    <img src="https://img2.teletype.in/files/d8/99/d899b63c-857d-4a72-9a72-13f96760811a.png" width="1014" />
  </figure>
  <h2 id="UwEe">З.Ы.</h2>
  <p id="6mIU">Хотелось бы тут добавить &quot;вишенку на торте&quot;, как мы можем автоматизировать UI тест и запускать не просто контейнер из образа нашего приложения, а запускать <strong>docker compose</strong>, чтобы не приходилось руками поднимать контейнеры базы данных. Но, <strong>Testcontainers </strong>для <strong>Java </strong>имеет фичу по работе с <strong>docker compose</strong> (<em>DockerComposeContainer</em>), а вот в .NET её ещё не завезли. Хотя всё, что делает <strong>docker compose</strong>, вы можете воспроизвести в коде теста: создать сеть, создать контейнеры базы данных.</p>

]]></content:encoded></item><item><guid isPermaLink="true">https://deploy2production.ru/weather-forecast-docker</guid><link>https://deploy2production.ru/weather-forecast-docker?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=deploy2production</link><comments>https://deploy2production.ru/weather-forecast-docker?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=deploy2production#comments</comments><dc:creator>deploy2production</dc:creator><title>WeatherForecast. Docker-изация.</title><pubDate>Sat, 18 Mar 2023 15:50:07 GMT</pubDate><media:content medium="image" url="https://img2.teletype.in/files/5c/a1/5ca1b2b3-94b2-41e0-9180-51e22f3fd668.png"></media:content><category>Weather Forecast</category><description><![CDATA[<img src="https://img3.teletype.in/files/27/74/27743ea4-f9b1-4602-8c47-638a015d32f4.png"></img>И так, у нас есть проект, подопытный кролик, который мы опубликовали нашими умелыми ручками на виртуальную машину с &quot;настоящими&quot; базами данных Postgres и  Redis. В очевидной простоте (скачать исходники на виртуальную машину, собрать, настроить демона) есть и такие же очевидные недостатки:]]></description><content:encoded><![CDATA[
  <h2 id="2FED">Предисловие</h2>
  <p id="DZwO">И так, у нас есть проект, <a href="https://deploy2production.ru/weather-forecast-dev" target="_blank">подопытный кролик</a>, который мы <a href="https://deploy2production.ru/weather-forecast-dev-deploy-selectel" target="_blank">опубликовали</a> нашими умелыми ручками на виртуальную машину с &quot;настоящими&quot; базами данных Postgres и  Redis. В очевидной простоте (скачать исходники на виртуальную машину, собрать, настроить демона) есть и такие же очевидные недостатки:</p>
  <ul id="FOg3">
    <li id="jk4f">ручная работа (= ошибки), которую можно автоматизировать скриптами. Но главное достоинство хорошего программиста - это лень. Если можно что-то не делать, лучше этого не делать.</li>
    <li id="OZFP">сложности с обновлением приложения. Вот вы запустили первую версию. Прошло время и у вас готова новая версия приложения, а денег вы ещё не заработали на новые сервера. Вам придется производить работы на &quot;горячем&quot; сервере. Огромная доза адреналина вам обеспечена. Сервер будет обслуживать клиентов и в это же время собирать новую версию. А ещё нужно внести изменения в настройки системы и NGINX. Страшно! Логично остановить работу приложения на время работ. И скорее всего вам придется это сделать, так как новая версия потребует обновление схемы баз данных. А делать миграции баз данных так, чтобы не требовалось останавливать приложение нужно уметь ещё. И это пол беды. А что если в новой версии обнаружились баги?! o_0 Нужно откатываться на предыдущую версию (вы же не удалили ветку в git с предыдущей версией o_0). Опять останов. Куча адреналина!</li>
    <li id="LHuT">и самое волшебное, что может с вами случиться - проект просто не соберется на сервере. Да, да. Такое может случиться. Например, вы забили закомитить какой-то файл. Или GitHub или какой-то другой репозиторий прилег, или забанен. Или сервер NuGet решил взять выходной. Или...</li>
  </ul>
  <p id="d48G">Тут-то и приходит нам на помощь Docker. Но давайте по порядку. Сначала нам нужно поместить наше приложение в контейнер и научиться работать с ним локально.</p>
  <h2 id="xsIV">Поехали</h2>
  <p id="dWJM">Исходники к этому посту находятся <a href="https://github.com/deploy2production/weather-forecast/tree/docker" target="_blank">тут</a>.</p>
  <p id="QRcW">Начнем мы с того, что удалим контейнеры Postgres и Redis, который ранее создавали. Наше приложение будет запускаться в контейнере и подключаться оно будет к базам данных, который тоже находятся в контейнерах. Настройка сети Docker-а, которая используется по-умолчанию, сеть <strong>bridge</strong>, позволяет адресовать контейнеры только через IP, что не удобно (нужно будет после запуска контейнера узнавать его IP). Подробности <a href="https://www.tutorialworks.com/container-networking/" target="_blank">тут</a>. По этому мы сначала создадим свою сеть в Docker командой:</p>
  <p id="34vs">docker network create weather-forecast-net</p>
  <p id="nvSt">Команды для создания контейнеров баз данных теперь будут выглядеть так (мы добавили параметр <strong>--net</strong>):</p>
  <pre id="up2z" data-lang="bash">docker run --name weather-forecast-postgres --net weather-forecast-net -p 5432:5432 -e POSTGRES_PASSWORD=postgres -d postgres</pre>
  <pre id="oVxF" data-lang="bash">docker run --name weather-forecast-redis --net weather-forecast-net -p 6379:6379 -d redis</pre>
  <p id="ZCtR">Для создания образа контейнера нашего приложения нужно создать в проекте файл <em><strong>Dockerfile</strong></em>:</p>
  <figure id="BpTq" class="m_original">
    <img src="https://img3.teletype.in/files/27/74/27743ea4-f9b1-4602-8c47-638a015d32f4.png" width="450" />
  </figure>
  <pre id="1tEa" data-lang="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=&quot;Host=weather-forecast-postgres;Username=postgres;Password=postgres;Database=postgres&quot;
ENV ConnectionStrings__Redis=&quot;weather-forecast-redis:6379&quot;

# Start App
ENTRYPOINT [&quot;dotnet&quot;, &quot;DeployToProduction.WeatherForecast.App.dll&quot;]</pre>
  <p id="q4WZ">В чем тут суть. </p>
  <p id="6N9K">Сначала мы используем образ &quot;<strong><em>mcr.microsoft.com/dotnet/sdk:7.0</em></strong>&quot; для сборки и публикации проекта: копируем исходники инструкцией <strong>COPY</strong>, запускаем сборку dotnet инструкцией <strong>RUN</strong>. </p>
  <p id="gTtn">Берем второй образ &quot;<strong><em>mcr.microsoft.com/dotnet/aspnet:7.0</em></strong>&quot;, в котором есть только run-time dotnet-а и apt.net, копируем в него результат сборки-публикации, то есть наше приложение, задаем переменные окружения инструкцией <strong>ENV</strong>, и указываем какую команду должен выполнить контейнер при старте инструкцией <strong>ENTRYPOINT</strong>. </p>
  <p id="poMD">Обратите внимание, как мы задаем строки подключения, имена серверов баз данных - это имена контейнеров (для этого мы и создавали свою сеть в Docker).</p>
  <p id="w7xu">Дополнительно, чтобы в образы не копировались &quot;ненужные&quot; файлы, радом с <strong><em>Dockerfile </em></strong>лежит файл <strong><em>.dockerignore</em></strong> - это список файлов и каталогов, который нужно проигнорировать при сборке образа.</p>
  <p id="Ap6d">Теперь открываем терминал в каталоге где лежит <strong><em>Dockerfile </em></strong>и выполняем команду сборки образа:</p>
  <pre id="WmCK" data-lang="bash">docker build -t weather-forecast-app-img -f Dockerfile .</pre>
  <figure id="RZfY" class="m_original">
    <img src="https://img1.teletype.in/files/c1/e8/c1e8bbda-6aff-41ef-b07b-fe661a319846.png" width="1115" />
  </figure>
  <p id="ltyB">Новый образ вы увидите в приложении Docker Desktop:</p>
  <figure id="6Ag7" class="m_original">
    <img src="https://img4.teletype.in/files/f0/2e/f02e98dc-4247-46dd-9b1b-93b09af366a3.png" width="960" />
  </figure>
  <p id="uyKr">Осталось запустить контейнер из этого образа (образы баз данных должны быть запущены):</p>
  <pre id="Azql" data-lang="bash">docker run --name weather-forecast-app --net weather-forecast-net -p 5022:80 -d weather-forecast-app-img</pre>
  <figure id="1Qlu" class="m_original">
    <img src="https://img2.teletype.in/files/d8/6c/d86c307c-874d-4cde-8367-33e608380ec4.png" width="960" />
  </figure>
  <p id="Iu6O">Проверяем в браузере:</p>
  <figure id="stPj" class="m_original">
    <img src="https://img2.teletype.in/files/1a/1c/1a1cf1f2-c8b5-48c2-91e5-753996f10d09.png" width="746" />
  </figure>
  <p id="l44W">Работает!</p>
  <p id="6bwV">Подытожим. Теперь мы можем оперировать Docker-образами как единицами деплоймента. Один раз собрали образ контейнера - теперь он запускается и работает везде одинаково. &quot;Развернуть&quot; приложение теперь - это одна команда запуска Docker-контейнера. Образы хранятся и версионируются в специальных репозиториях.</p>
  <h2 id="fKOZ">З.Ы.</h2>
  <p id="3WpC">А теперь самое вкусненькое =) Помните, ранее для UI теста мы сначала запускали само приложение, а потом запускали тест. Теперь мы можем убрать это неудобство предоставив работу по запуску приложения библиотеке Testcontainers (у нас же теперь есть образ нашего приложения).</p>
  <figure id="QPAn" class="m_original">
    <img src="https://img4.teletype.in/files/f7/01/f701ffa2-8c76-4ea6-b3db-95bb3da83e76.png" width="656" />
  </figure>
  <figure id="QJh4" class="m_original">
    <img src="https://img1.teletype.in/files/c1/32/c132c625-7f78-42c3-812a-036f2b3e34a5.png" width="570" />
  </figure>

]]></content:encoded></item><item><guid isPermaLink="true">https://deploy2production.ru/weather-forecast-dev-deploy-selectel</guid><link>https://deploy2production.ru/weather-forecast-dev-deploy-selectel?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=deploy2production</link><comments>https://deploy2production.ru/weather-forecast-dev-deploy-selectel?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=deploy2production#comments</comments><dc:creator>deploy2production</dc:creator><title>WeatherForecast. Dev. Deploy. Selectel.</title><pubDate>Sun, 05 Mar 2023 08:01:03 GMT</pubDate><media:content medium="image" url="https://img2.teletype.in/files/95/d2/95d21a13-2160-48ca-8cc3-7a624645c7cf.png"></media:content><category>Weather Forecast</category><description><![CDATA[<img src="https://img3.teletype.in/files/a0/2a/a02a2454-ff1b-447e-80eb-31187f1443d4.png"></img>И так, у нас есть проект в стадии разработки, и я хочу проверить как он будет разворачиваться на виртуальном сервере с подключением к реальным базам данным. Заодно, ещё раз потренируемся.]]></description><content:encoded><![CDATA[
  <p id="5SiT">И так, у нас есть <a href="https://deploy2production.ru/weather-forecast-dev" target="_blank">проект </a>в стадии разработки, и я хочу проверить как он будет разворачиваться на виртуальном сервере с подключением к реальным базам данным. Заодно, ещё раз потренируемся.</p>
  <p id="9m8S"><a href="https://deploy2production.ru/vm-selectel-test" target="_blank">Создаем </a>виртуальную машину. Только в этот раз в настройках сети выбираем &quot;Приватная подсеть + 1 публичный IP&quot;. Наш сервер будет доступен по публичному IP, а базы данных будут находиться в приватной сети, и будут доступны только внутри неё. По соображениям безопасности лучше избегать выставления баз данных наружу.</p>
  <figure id="zNjI" class="m_original">
    <img src="https://img3.teletype.in/files/a0/2a/a02a2454-ff1b-447e-80eb-31187f1443d4.png" width="534" />
  </figure>
  <p id="7Cj2">В этот раз нам понадобится размер диска побольше, потому что 5 ГБ не хватит, я увеличил размер диска до 7ГБ.</p>
  <p id="zKLQ">После создания сервера мы видим в консоли 2 IP адреса: первый - публичный, второй - внутри приватной сети. </p>
  <p id="H2mg"></p>
  <figure id="ySZ9" class="m_original">
    <img src="https://img3.teletype.in/files/6f/9e/6f9e629a-c5cd-43aa-a4d0-9c5f8efe2d69.png" width="793" />
  </figure>
  <p id="zb54">Далее настраиваете подключение по <a href="https://deploy2production.ru/vm-ssh-connect" target="_blank">SSH</a>. </p>
  <p id="gkD0">Затем устанавливаете ПО:<a href="https://deploy2production.ru/vm-install-soft" target="_blank"> NGINX и ufw</a>, <a href="https://deploy2production.ru/try-wsl-dotnet" target="_blank">dotnet</a></p>
  <p id="SHfc">Ещё нам понадобится программа Git, чтобы скачать исходники с GitHub. Устанавливаете командой:</p>
  <pre id="CoVm" data-lang="bash">sudo apt install git -y</pre>
  <p id="ZoPT">Теперь скачиваем исходники проекта, я качаю только ветку dev, последний коммит:</p>
  <pre id="Pmlt" data-lang="bash">git clone --depth 1 -b dev https://github.com/deploy2production/weather-forecast.git</pre>
  <p id="VtcH">Переходим в каталог проекта и собираем его для публикации:</p>
  <pre id="gDi2" data-lang="bash">cd weather-forecast/</pre>
  <pre id="tDxK" data-lang="bash">dotnet publish DeployToProduction.WeatherForecast.App --configuration Release</pre>
  <p id="9YTX"></p>
  <p id="fzXi">Теперь нам нужно создать базы данных. Заходим в панель управления Selectel, в левом меню выбираем &quot;Облачная платформа&quot; -&gt; &quot;Базы данных&quot;, жмем &quot;Создать кластер&quot;.</p>
  <p id="8E6C">Первой базой данных мы создадим Postgres.</p>
  <figure id="1162" class="m_original">
    <img src="https://img3.teletype.in/files/68/4e/684e792b-3841-4ba6-88e2-bd4f9aadb6d6.png" width="743" />
  </figure>
  <p id="oqQE">Выбираем &quot;Произвольная&quot; конфигурация и ставим минимальные ресурсы, мы тренируемся, тратить лишние деньги нам ни к чему. Реплики нам тоже пока не нужны.</p>
  <figure id="OYn8" class="m_original">
    <img src="https://img4.teletype.in/files/7a/ca/7aca8578-b6c9-40e5-bb31-edb3ea67661a.png" width="772" />
  </figure>
  <p id="u9Lw">В настройках сети выбираем ранее созданную с виртуальной машиной подсеть.</p>
  <figure id="ENmk" class="m_original">
    <img src="https://img1.teletype.in/files/07/24/0724a015-d6af-48b2-9c3b-b2c673050894.png" width="748" />
  </figure>
  <p id="uHL7">И жмем &quot;Создать кластер баз данных&quot;.</p>
  <figure id="Es8E" class="m_original">
    <img src="https://img3.teletype.in/files/ee/5c/ee5c68f5-d296-40d8-9720-a207b4ed4627.png" width="783" />
  </figure>
  <p id="UedU"></p>
  <p id="Dpev">Второй базой данных создаем Redis. Также выбираем минимальные ресурсы.</p>
  <figure id="VQD9" class="m_original">
    <img src="https://img1.teletype.in/files/cb/b6/cbb6cf2b-2b6a-494f-9190-59c5cb4207ea.png" width="772" />
  </figure>
  <p id="16aM">Выбираем политику вытеснения. И генерируем пароль. С паролем тут аккуратно. Нам нужен пароль без запятых (замените запятые на другой символ). Сохраните пароль.</p>
  <figure id="X5f4" class="m_original">
    <img src="https://img3.teletype.in/files/64/3b/643b0388-dc1a-4b7d-8830-adc8b208b822.png" width="770" />
  </figure>
  <p id="vvdD">Выбираем подсеть. Жмем &quot;Создать кластер баз данных&quot;.</p>
  <figure id="LOqh" class="m_original">
    <img src="https://img3.teletype.in/files/2e/a5/2ea581a3-10e6-4840-81c4-d98568c8fa8a.png" width="779" />
  </figure>
  <p id="IUye">Ждем пока базы данных будут созданы.</p>
  <figure id="chNY" class="m_original">
    <img src="https://img4.teletype.in/files/3d/9e/3d9e3a02-4fbf-4ef1-940e-f84bd894accc.png" width="804" />
  </figure>
  <p id="ev0t">Теперь нужно настроить Postgres: создать пользователя и базу данных.</p>
  <p id="TgaZ">Кликаем на строку &quot;WeatherForecastPgsql&quot; в списке кластеров, выбираем вкладку &quot;Пользователь&quot;.</p>
  <figure id="l61G" class="m_original">
    <img src="https://img1.teletype.in/files/40/f9/40f91150-a205-4180-bf41-62fa2d14e87e.png" width="785" />
  </figure>
  <p id="LCk0">Жмем &quot;Создать пользователя&quot;. Вводим имя и пароль.</p>
  <figure id="72Ea" class="m_original">
    <img src="https://img2.teletype.in/files/19/29/1929a05d-fa95-44b5-b561-c4fc403ac3f5.png" width="789" />
  </figure>
  <p id="orw3">Ждем завершения операции:</p>
  <figure id="Xv4y" class="m_original">
    <img src="https://img2.teletype.in/files/58/bb/58bbc8b0-2b10-4a3d-b154-b254ed9914fe.png" width="781" />
  </figure>
  <p id="eNkf">Открываем вкладку &quot;Базы данных&quot;, жмем &quot;Создаем базу данных&quot;.</p>
  <figure id="US7k" class="m_original">
    <img src="https://img1.teletype.in/files/86/95/8695afcb-60b1-46b0-b264-70875f1a10bb.png" width="751" />
  </figure>
  <p id="0MAE">Выбираем имя, и влаельца, жмем &quot;Создать&quot;.</p>
  <figure id="bxGm" class="m_original">
    <img src="https://img1.teletype.in/files/03/a2/03a27420-9a49-4744-b2b6-dfc32674a835.png" width="798" />
  </figure>
  <p id="zX1y">Готово.</p>
  <figure id="ksxN" class="m_original">
    <img src="https://img3.teletype.in/files/e4/60/e460ff0c-e4b3-427f-86fa-1e5f02a508a3.png" width="789" />
  </figure>
  <p id="TI7i">На вкладке &quot;Настройки&quot; вы можете увидеть как подключиться к базе данных, нам эта информация пригодится при настройке ConnectionStrings нашего сервиса:</p>
  <figure id="cRAp" class="m_original">
    <img src="https://img3.teletype.in/files/e0/2e/e02e22e5-74c5-4c49-8a19-38f0d892284b.png" width="769" />
  </figure>
  <figure id="H4TI" class="m_original">
    <img src="https://img2.teletype.in/files/5e/d4/5ed4f982-22eb-4f75-84b6-95cab9dc6803.png" width="781" />
  </figure>
  <p id="yquF">С настройкой <a href="https://deploy2production.ru/vm-daemon" target="_blank">демонов</a> вы уже знакомы. Кратко:</p>
  <pre id="6J5u" data-lang="bash">sudo nano /etc/systemd/system/weatherforecast.service</pre>
  <p id="op25">Меняем в настройках путь к каталогу куда проект был опубликован, имя dll-ки. И добавляем два ключа <strong><em>Environment </em></strong>(ConnectionStrings + два символа _  + название = значение в кавычках):</p>
  <pre id="tQNn" data-lang="toml">[Unit]
Description=weatherforecast

[Service]
WorkingDirectory=/home/aseverin/weather-forecast/DeployToProduction.WeatherForecast.App/bin/Release/net7.0/publish
ExecStart=dotnet ./DeployToProduction.WeatherForecast.App.dll
Restart=always
# Restart service after 10 seconds if the dotnet service crashes:
RestartSec=10
KillSignal=SIGINT
SyslogIdentifier=weatherforecast
User=aseverin
Environment=ASPNETCORE_ENVIRONMENT=Production
Environment=DOTNET_PRINT_TELEMETRY_MESSAGE=false
Environment=ASPNETCORE_URLS=http://127.0.0.1:5022
Environment=ConnectionStrings__Postgres=&quot;Host=192.168.0.115;Port=6432;UserName=webapp;Password=*******;Database=weatherforecast&quot;
Environment=ConnectionStrings__Redis=&quot;192.168.0.136:6379,password=********&quot;


[Install]
WantedBy=multi-user.targetmulti-user.targetmulti-user.target</pre>
  <p id="06mh">Запускаем:</p>
  <pre id="lgKE" data-lang="bash">sudo systemctl enable weatherforecast.service
sudo systemctl start weatherforecast.service</pre>
  <p id="eaJT">Настраиваем NGINX:</p>
  <pre id="q6Gy" data-lang="bash">sudo nano /etc/nginx/sites-available/default</pre>
  <p id="KiP0">Оставляем только один сервер для обработки запросов:</p>
  <pre id="dcbK" data-lang="nginx">upstream backend {
        server 127.0.0.1:5022;
}
server {
        location / {
                proxy_pass http://backend;
        }
}</pre>
  <p id="7VjW"></p>
  <p id="vkDi">Перезапускаем NGINX:</p>
  <pre id="p7th" data-lang="bash">sudo service nginx restart</pre>
  <p id="yYas">Результат:</p>
  <figure id="ZHBC" class="m_original">
    <img src="https://img2.teletype.in/files/d2/9d/d29dc9db-76d8-4653-ad43-c72276f45d30.png" width="746" />
  </figure>
  <p id="EHWG">И давайте бонусом настроим DNS для нашего проекта. </p>
  <p id="MJLm">Я открываю панель управления доменами в REG.RU, выбираю домен  deploy2production.ru, жму &quot;Добавить запись&quot;.</p>
  <figure id="8lTL" class="m_original">
    <img src="https://img1.teletype.in/files/c7/51/c751a806-11a1-40f7-838e-af574af4101a.png" width="1204" />
  </figure>
  <p id="CcHG">Выбираю запись &quot;А&quot;</p>
  <figure id="XIWC" class="m_original">
    <img src="https://img4.teletype.in/files/72/e4/72e43d23-b431-4216-9635-a77e08faafa5.png" width="346" />
  </figure>
  <p id="ITYM">Ввожу под-домен weather-forecast и публичный IP адрес моей виртуальной машины, жму &quot;Готово&quot;: </p>
  <figure id="VhVU" class="m_original">
    <img src="https://img1.teletype.in/files/83/8e/838e965f-8058-455c-8c50-f0657ee8b166.png" width="601" />
  </figure>
  <p id="MMsB">Ждем, когда изменения DNS вступят в силу. Проверяем:</p>
  <figure id="ts4Z" class="m_original">
    <img src="https://img4.teletype.in/files/3a/0b/3a0b465a-5c82-4ce2-a86a-35264965a75a.png" width="746" />
  </figure>
  <p id="GEn3">Готово.</p>
  <h2 id="6Uvr">З.Ы.</h2>
  <p id="L9QE">Не забываем после экспериментов и тренировок удалять не нужные ресурсы (виртуальные машины, базы данных)</p>

]]></content:encoded></item><item><guid isPermaLink="true">https://deploy2production.ru/weather-forecast-dev</guid><link>https://deploy2production.ru/weather-forecast-dev?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=deploy2production</link><comments>https://deploy2production.ru/weather-forecast-dev?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=deploy2production#comments</comments><dc:creator>deploy2production</dc:creator><title>WeatherForecast. Dev.</title><pubDate>Sat, 04 Mar 2023 19:38:06 GMT</pubDate><media:content medium="image" url="https://img3.teletype.in/files/a7/1d/a71de85f-0dd5-41bf-8a42-425b99f75d8c.png"></media:content><category>Weather Forecast</category><description><![CDATA[<img src="https://img2.teletype.in/files/d9/fc/d9fc529c-502d-414b-8093-2e274f6cb54a.png"></img>Для дальнейшего погружения в тонкости разворачивания проектов нам понадобится подопытный кролик - веб-проект с типичной архитектурой, но с простой бизнес логикой и легко читаемым кодом. Мне пришел в голову сервис, который &quot;предсказывает&quot; случайную погоду.]]></description><content:encoded><![CDATA[
  <h2 id="No0b">Предисловие</h2>
  <p id="Cpfn">Для дальнейшего погружения в тонкости разворачивания проектов нам понадобится подопытный кролик - веб-проект с типичной архитектурой, но с простой бизнес логикой и легко читаемым кодом. Мне пришел в голову сервис, который &quot;предсказывает&quot; случайную погоду.</p>
  <figure id="hL8W" class="m_column">
    <img src="https://img2.teletype.in/files/d9/fc/d9fc529c-502d-414b-8093-2e274f6cb54a.png" width="746" />
  </figure>
  <p id="mI5i">Не смотря на свою банальность, в проекте используется база данных Postgres для хранения &quot;предсказаний&quot;, более того предсказания кэшируются в базе данных Redis. Это типичная связка, &quot;бд-кэш&quot;, используется для облегчения нагрузки на базу данных и ускорения ответа пользователю.</p>
  <p id="ljw5">Исходный код всего проекта находится на <a href="https://github.com/deploy2production/weather-forecast" target="_blank">GitHub</a>. Я буду фиксировать код на состояние каждого поста в отдельных ветках. Код для этого поста находится в ветке <a href="https://github.com/deploy2production/weather-forecast/tree/dev" target="_blank">dev</a>.</p>
  <h2 id="sy6H">Поехали</h2>
  <p id="hclC">Посмотрим из чего состоит проект</p>
  <figure id="Lnmv" class="m_original">
    <img src="https://img3.teletype.in/files/20/5f/205fd023-6097-4fa4-aa19-17f4bf39c4fb.png" width="451" />
  </figure>
  <p id="1GP5"><strong>DeployToProduction.WeatherForecast.App</strong> - ASP.NET Core приложение, которое использует только Razor-pages.</p>
  <figure id="97Nn" class="m_original">
    <img src="https://img3.teletype.in/files/ac/0d/ac0d2892-4fbe-4b7d-83d2-272e54e34acd.png" width="865" />
  </figure>
  <figure id="70VA" class="m_original">
    <img src="https://img3.teletype.in/files/ec/e1/ece187a3-13d9-4cb9-8342-63c9539c4aa4.png" width="655" />
  </figure>
  <p id="M9MK"><strong>DeployToProduction.WeatherForecast.Core</strong> - библиотека с модельками</p>
  <figure id="J94w" class="m_original">
    <img src="https://img3.teletype.in/files/ae/56/ae565ce2-6f97-4f28-a7cc-c54135e757c1.png" width="566" />
  </figure>
  <figure id="r8IA" class="m_original">
    <img src="https://img2.teletype.in/files/59/03/59034cbe-6286-4c79-ab42-5c628a17ee29.png" width="731" />
  </figure>
  <figure id="Vjei" class="m_original">
    <img src="https://img4.teletype.in/files/f5/dc/f5dc9952-935b-471f-b247-1e8ee16c75e9.png" width="565" />
  </figure>
  <figure id="OR64" class="m_original">
    <img src="https://img1.teletype.in/files/c4/0e/c40ed26f-7187-48b1-8c3c-d60f90a0a498.png" width="624" />
  </figure>
  <p id="XMbG"></p>
  <p id="O3Ko"><strong>DeployToProduction.WeatherForecast.Data</strong> - библиотека описывающая абстракцию доступа к данным.</p>
  <figure id="XkYB" class="m_original">
    <img src="https://img3.teletype.in/files/ae/f9/aef92ec9-fb71-4e37-b012-77ed273e0ed6.png" width="549" />
  </figure>
  <figure id="nf3n" class="m_original">
    <img src="https://img1.teletype.in/files/07/67/07673ae4-044f-4ad5-951f-5e6efbe75d58.png" width="679" />
  </figure>
  <p id="TX8A"><strong>DeployToProduction.WeatherForecast.Data.Psql</strong> - библиотека с реализацией доступа к данным через базу данных Postgres.</p>
  <p id="J44n">Тут нужно поговорить подробнее. Я хотел бы отметить два момента:</p>
  <ol id="quKk">
    <li id="GV33">Данные для бизнеса - это ключевой актив. Работу с данными нужно организовывать тщательнейшим образом, сто раз всё продумать, сто раз всё проверить, сделать все возможные бэкапы, проверить, что бэкапы работают, максимально автоматизировать, чтобы избежать человеческий фактор, проверить автоматизацию... В общем вы поняли, данные - это серьезно.</li>
    <li id="KdQ4">ORM - инструмент, который облегчает работу с данными на старте и многое скрывает от разработчика, в этом его и плюс и минус. Базы данных - это очень сложный инструмент и в устройстве работы конкретной базы данных в конкретном проекте придется разбираться, когда бизнес выйдет в свет (вырастит нагрузка, увеличится количество функционала, изменения требований посыпятся одним за одним). Поэтому, я считаю, стоит в пет-проектах потренироваться работать с базовым уровнем - SQL.</li>
  </ol>
  <p id="Rcaj">Создание схемы базы данных описывается в sql-файле:</p>
  <figure id="iKQ2" class="m_original">
    <img src="https://img3.teletype.in/files/27/35/2735da65-2fcf-4f81-9c33-698b6e0584b5.png" width="680" />
  </figure>
  <p id="SCID">Для выполнения этого sql используется библиотека <a href="https://dbup.readthedocs.io/en/latest/" target="_blank">DbUp</a>, которая помогает с созданием и миграцией схем баз данных:</p>
  <figure id="yaDs" class="m_original">
    <img src="https://img4.teletype.in/files/fc/33/fc3321cc-bd01-4bef-b0ea-9bc6cf0295a5.png" width="732" />
  </figure>
  <p id="33Lq">Это очень простой по сути подход, со 100% контролем происходящего - вы пишите SQL-скрипты создания всех объектов схемы БД и их последующую модификацию/миграцию. А библиотека DbUp обеспечит применение этих скриптов. Делает это она тоже довольно просто. В БД создается таблица <strong><em>schemaversions</em></strong>, в которую записываются все примененные скрипты. Таким образом каждый скрипт будет выполнен только один раз.</p>
  <figure id="uNUh" class="m_original">
    <img src="https://img4.teletype.in/files/35/06/35061522-069b-47e6-ae42-fcaf88877f8f.png" width="898" />
  </figure>
  <p id="F5c9">Ну и сам код работы с базой данных я не стал нагружать транзакциями, специфическими для Postgres конструкциями, а написал наиболее простой вариант, хоть и не корректный с точки зрения конкурентной записи в БД:</p>
  <figure id="36so" class="m_original">
    <img src="https://img2.teletype.in/files/51/9a/519a2ff1-e9f7-4bf7-a498-b8a4b178e0cb.png" width="1277" />
  </figure>
  <figure id="oeXs" class="m_original">
    <img src="https://img1.teletype.in/files/c8/e6/c8e69194-6044-4d75-b37f-a1be7f2dfac0.png" width="842" />
  </figure>
  <p id="lJy6"><strong>DeployToProduction.WeatherForecast.Data.Redis</strong> - библиотека с реализацией доступа к данным через кэш в Redis.</p>
  <p id="olzg">Кэширование в Redis я делаю с помощью паттерна &quot;обертки&quot;:</p>
  <figure id="Ri50" class="m_original">
    <img src="https://img3.teletype.in/files/27/68/2768f6ac-f239-498a-90d5-e9482bf0187b.png" width="720" />
  </figure>
  <figure id="QBqq" class="m_original">
    <img src="https://img4.teletype.in/files/7c/48/7c48d645-d871-4f8a-9864-f39a788ce231.png" width="966" />
  </figure>
  <p id="W6Mv">Вернемся к основному приложению <strong>DeployToProduction.WeatherForecast.App</strong> и его запуску. Как вы поняли для запуска приложения нам понадобится Postgres и Redis. Их, конечно, можно установить в вашу систему, но так уже никто не носит. Для разворачивания подобных зависимостей сегодня используют Docker, с которым вы уже <a href="https://deploy2production.ru/try-docker-dotnet" target="_blank">знакомы</a>. Базового уровня &quot;запустить контейнер из командной строки&quot; вам будет достаточно. Необходимые команды есть в README.md:</p>
  <figure id="i35M" class="m_original">
    <img src="https://img4.teletype.in/files/b7/1c/b71ca010-e465-410d-bd03-02b903eb5bb0.png" width="882" />
  </figure>
  <p id="Doay">Для подключения к базам данных вам нужно будет прописать <strong><em>ConnectionStrings </em></strong>в <strong><em>appsettings.Development.json</em></strong>:</p>
  <figure id="jlLL" class="m_original">
    <img src="https://img1.teletype.in/files/48/82/4882a8a5-7884-4b7a-a20b-992c64173328.png" width="786" />
  </figure>
  <p id="gyRt">На этапе начальной разработки, при старте приложения я вызываю &quot;установку&quot; базы данных Postgres, что удобно на страте, но не годится для проекта, который ушел в прод, но об этом мы поговорим позже.</p>
  <figure id="KXGm" class="m_original">
    <img src="https://img2.teletype.in/files/94/95/9495653e-1698-4df1-ac1f-8b592e3725f1.png" width="853" />
  </figure>
  <p id="ucay">Ну и конечно тесты. Без тестов нельзя. Логика у нас тривиальная и не требует юнит-тестов. А вот написание интеграционных и UI тестов показать хочется, потому что часто это дело кажется сложным и откладывается на потом, а потом уже поздно. Юнит тесты помогают держать дизайн кода чистым, а интеграционные тесты помогают держать чистой архитектуру. Поэтому тесты нужно писать сразу.</p>
  <p id="wzXO">Для тестирования работы с базами данных я использую тот же Docker, но с помощью библиотеки <a href="https://dotnet.testcontainers.org/" target="_blank">Testcontainers</a>. (Да, я хотел показать ещё один плюс от владения инструментом Docker)</p>
  <p id="rhOu"><strong>DeployToProduction.WeatherForecast.Data.Psql.IntegrationTests</strong></p>
  <figure id="0wnt" class="m_original">
    <img src="https://img3.teletype.in/files/a3/03/a303dee2-e13d-40f8-8e94-b5820fc32f5f.png" width="745" />
  </figure>
  <figure id="h8tV" class="m_original">
    <img src="https://img3.teletype.in/files/6b/0e/6b0ebae3-4d58-4538-9723-e929f8c150d4.png" width="814" />
  </figure>
  <p id="cs87"><strong>DeployToProduction.WeatherForecast.Data.Redis.IntegrationTests</strong></p>
  <figure id="WXmQ" class="m_original">
    <img src="https://img3.teletype.in/files/6b/65/6b65c06c-99cc-4dbf-96f8-064ffd045100.png" width="853" />
  </figure>
  <p id="O1DK">Код тестов я специально оставил примитивным и с повторениями, чтобы было проще в нем разобраться. В боевом проекте задача по созданию тестового контейнера выносится в базовый класс, и код теста остаётся чистеньким.</p>
  <p id="L3dP">Перед запуском тестов выключайте контейнеры, созданные для разработки, и запускайте тесты по одному, иначе будет ошибка из-за конфликта портов.</p>
  <p id="HfLj">При запуске этих тестов вы можете заметить, как в приложении Docker Desktop создаются и удаляются контейнеры из ваших тестов + 1 вспомогательных контейнер:</p>
  <figure id="4fvI" class="m_original">
    <img src="https://img3.teletype.in/files/6f/0f/6f0f076b-3970-4341-b219-92d0fd320000.png" width="956" />
  </figure>
  <p id="NGRA"><strong>DeployToProduction.WeatherForecast.App.UITests </strong>- тесты UI с помощью Selenium.</p>
  <p id="48yD">Автоматизировать запуск проекта ASP.NET из тестов - задача не тривиальная, но наиболее простым способом решается с помощью Docker-a и Docker Compose. И вернемся мы к этому вопросу, когда будет говорить о контейнеризации нашего приложения. А пока, я вам предлагаю вручную запустить проект без отладки (предварительно стартанув контейнеры для разработки) и потом запустить тест.</p>
  <figure id="AOTr" class="m_original">
    <img src="https://img2.teletype.in/files/9f/84/9f845a4c-f541-466e-8ce5-6b31d1435b3f.png" width="737" />
  </figure>
  <p id="K0IF">На этом наше знакомство с проектом завершено. Далее мы запустим проект в <a href="https://selectel.ru/?ref_code=a00c5d3530" target="_blank">Selectel </a>с управляемыми базами данных Postgres и Redis.</p>

]]></content:encoded></item><item><guid isPermaLink="true">https://deploy2production.ru/vm-delete</guid><link>https://deploy2production.ru/vm-delete?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=deploy2production</link><comments>https://deploy2production.ru/vm-delete?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=deploy2production#comments</comments><dc:creator>deploy2production</dc:creator><title>Виртуальная машина. Удооолить.</title><pubDate>Tue, 21 Feb 2023 19:49:25 GMT</pubDate><media:content medium="image" url="https://img3.teletype.in/files/23/68/2368485d-d3b3-49be-b111-66a3d724db2b.png"></media:content><category>Виртуальная машина</category><description><![CDATA[<img src="https://img3.teletype.in/files/a5/63/a5639650-c667-4cb8-a188-59dc3d2ca5c1.png"></img>Наигравшись с тестовой виртуальной машиной, хороша идея - выключить и удалить её, чтобы денюшки не капали.]]></description><content:encoded><![CDATA[
  <p id="IqSW">Наигравшись с тестовой виртуальной машиной, хороша идея - выключить и удалить её, чтобы денюшки не капали.</p>
  <p id="2RHq">Заходим в панель управления Selectel, в левом меню выбираем &quot;Облачная платформа&quot; -&gt; &quot;Серверы&quot;, в списке выбираем свой сервер и жмем в &quot;Выключить&quot;.</p>
  <figure id="E5CJ" class="m_original">
    <img src="https://img3.teletype.in/files/a5/63/a5639650-c667-4cb8-a188-59dc3d2ca5c1.png" width="1228" />
  </figure>
  <figure id="Dn5B" class="m_original">
    <img src="https://img3.teletype.in/files/a1/fb/a1fb73b7-8829-4fd6-885f-ed0abf86ce0a.png" width="454" />
  </figure>
  <p id="1535">Ждем изменения статуса.</p>
  <figure id="xcxl" class="m_original">
    <img src="https://img1.teletype.in/files/4a/c5/4ac5257f-5952-4c84-af43-bbbfbac1d743.png" width="263" />
  </figure>
  <p id="3Xhv">Копируем имя сервера. Нажимаем &quot;Удалить сервер&quot;. В диалоге выбираем все галочки и вставляем скопированное имя сервера. &quot;Удалить&quot; </p>
  <figure id="P7FN" class="m_original">
    <img src="https://img4.teletype.in/files/bb/6e/bb6eebd0-e5e7-4049-baa3-0f31244f2292.png" width="315" />
  </figure>
  <p id="YoeR"></p>
  <figure id="Ns7p" class="m_original">
    <img src="https://img3.teletype.in/files/e7/be/e7bea3e6-a6c6-4d1a-be48-5ab4587106ea.png" width="449" />
  </figure>
  <p id="De6o">Список пуст:</p>
  <figure id="MO3Y" class="m_column">
    <img src="https://img2.teletype.in/files/19/30/19301061-b771-4734-979b-a308802763de.png" width="784" />
  </figure>

]]></content:encoded></item></channel></rss>