Всем привет! Когда я узнаю, что человек передо мной начинает изучать c# - первым делом я его спрашиваю, как ему язык, на чем раньше программировал и прочее. И в какой то момент разговоры доходят до докера\пайплайнов => многие ребята (которые не пробовали это раньше) начинают нехотя избегать эту тему, считая её чересчур скучной, странной и вообще "это уже какой то девопс". Хотя на деле - зная базово, что значат папки в твоем проекте на компе - можно освоить базовые основы красивой работы докера (+ можно пришить пайплайны, основной мотив у них один). Поэтому сегодня я попытаюсь привлечь ваше внимание к базовым командам dotnet`а.

Все изучать и делать будем сразу в докере. Создаем такой докерфайл:

FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
COPY . /app

Сдк и готового проекта нам хвтаит. Кстати, а на чем будем "тренироваться"? На простом консольном проекте с 1 нугет пакетом + работает в команде с еще одним проектом (типо для моделек), попробуем воссоздать "реальную" рабочую обстановку.

Выглядит это дело как то так. В первых 2 папках наши проеты.

Наше приложение:

using Extensions;
using Newtonsoft.Json;

namespace ConsoleApp1;

class Program
{
    static void Main(string[] args)
    {
        var model = new Model(){a = 2};
        string json = JsonConvert.SerializeObject(model, Formatting.Indented);
        var deserializedModel = JsonConvert.DeserializeObject<Model>(json);
        Console.WriteLine($"{deserializedModel.a}");
    }
}

Теперь делаем докербилд и запускаем приложение.

Из интересного будет что-то вроде этого. Нужных нугетов, сборок и всего остального у нас нет. Начинаем с самого начала: устанавливаем нужные нугеты. делаем команду в папке Console.App1 для нашего основного приложения - dotnet restore ConsoleApp1.csproj. Получаем скаченный нугет пакет на машине:

Также появились доп файлы к проектам, но нас пока это не интересует.

Далее делаем dotnet build ConsoleApp1.csproj.

Появились нужные длл для нашего проекта! Осталось "опубликовать" наше приложение, поместив в папку, например main, всё что нужно нашему приложению.

Делаем dotnet publish ConsoleApp1.csproj -o /main (флаг -o говорит куда сложить все файлы, делаем это для удобства)

Появилась папка main и нужным нам длл файлик. Перейдем в папку и запустим проект dotnet ConsoleApp1.dll

# dotnet ConsoleApp1.dll
2

Ура! Приложение работает. Думаете как долго и нудно? На самом деле можно использовать только команду dotnet publish ConsoleApp1.csproj -o /main - под капотом будет билд + рестор. Но полную "последовательность" создания нашего основого dll файла нам знать нужно. Теперь научимся красиво писать докерфайлы с новыми знаниями:

FROM mcr.microsoft.com/dotnet/runtime:8.0 AS base
WORKDIR /app

FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY . .
RUN dotnet restore "ConsoleApp1/ConsoleApp1.csproj"

WORKDIR /src/ConsoleApp1
RUN dotnet build ConsoleApp1.csproj --no-restore -c Release

FROM build AS publish
RUN dotnet publish ConsoleApp1.csproj --no-build -c Release -o /app/publish

FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "ConsoleApp1.dll"]

Что же тут происходит? Мы используем несколько этапов сборки aka многоступенчатую сборку, чтобы максимально уменшить итоговый контейнер. Сначала берем ТОЛЬКО рантайм, уже без всего нужного нам сдк. Далее в следующей сборке восстанавливаем зависимости. После мы делаем билд с флагом -no--restore, чтобы исключить повторный рестор (это экономит время, позволяя на шаги разбить процесс сборки), далее ставим конфигурацию на релиз и делаем публикацию нашего приложения в папку publish. Возвращаясь к 1 образу, копируем нужную нам итоговую папку publish из предыдущего действия. Запускам наше приложение. Таким образом имеем в контейнере:

Только нужные файлы для нашего приложения, даже нет установленных нугет пакетов как это делается через dotnet restore. Только готовый длл в одной папке. Как удобно! И главное, все разбито по полочкам.

Теперь перейдем к ci/cd.

Тут сразу переходим к самому простому (и бессмысленному) пайплану:

name: .NET CI/CD Pipeline

on:
  push:
    branches:
      - main
  pull_request:
    branches:
      - main

jobs:
  build:
    name: Build & Test
    runs-on: ubuntu-latest
    
    steps:
      - name: Checkout репозитория
        uses: actions/checkout@v4

      - name: Кэш NuGet пакетов
        uses: actions/cache@v3
        with:
          path: ~/.nuget/packages
          key: nuget-${{ runner.os }}-${{ hashFiles('**/packages.lock.json') }}
          restore-keys: nuget-${{ runner.os }}-

      - name: Установка .NET SDK
        uses: actions/setup-dotnet@v4
        with:
          dotnet-version: '8.0'

      - name: Восстановление зависимостей
        run: dotnet restore ConsoleApp1/ConsoleApp1.csproj

      - name: Сборка проекта
        run: dotnet build ConsoleApp1/ConsoleApp1.csproj --configuration Release --no-restore

      - name: Паблиш проекта
        run: dotnet publish ConsoleApp1/ConsoleApp1.csproj --configuration Release --no-build -o pub

      - name: Сохранение артефактов сборки
        uses: actions/upload-artifact@v4
        with:
          name: ConsoleApp1-artifact
          path: pub

  deploy:
    name: ? Deploy
    needs: build
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'

    steps:
      - name: ? Загрузка артефактов
        uses: actions/download-artifact@v4
        with:
          name: ConsoleApp1-artifact
          path: deploy/

      - name: Просто хочу видеть 2
        run: dotnet /home/runner/work/Rep/Rep/deploy/ConsoleApp1.dll

Получаем:

Более подробно этот файл можно разобрать с гпт. Но моменты, которые вы должны заметить: берем нугет пакеты в кеш (чтобы потом каждый раз не скачивали их), делаем также рестор, билд, паблиш, не забываем флаги. Сохраянем артефакт - просто готовыую папку с нужными файлами, её мы возьмём в другом джобе (логика, как у многоступенчатого докера). И в конце просто запустим приложение (не запутайтесь в папках, послежняя строка - пример, где лежит наш итоговый основной длл). Пайплайном это не назовёшь, но как базово обращаться со сборкой, вы узнали.

Комментарии (11)


  1. serhio_ribeira
    20.02.2025 12:05

    если все же оставить команду dotnet restore то у нас останется кеш с нугетами (по-крайней мере если мы используем Canico для сборки докера на CI), не уверен что при dotnet publish скачанные нугеты останутся в кеше


    1. alhimik45
      20.02.2025 12:05

      Под кешем имеется в виду закешированный layer контейнера? Но он же инвалидируется из-за `COPY . .` и кроме вырожденных случаев переиспользоваться не будет


  1. alhimik45
    20.02.2025 12:05

    FROM mcr.microsoft.com/dotnet/sdk:8.0 AS publish
    WORKDIR /src
    COPY . .
    RUN dotnet publish ConsoleApp1/ConsoleApp1.csproj -c Release -o /app/publish
    
    FROM mcr.microsoft.com/dotnet/runtime:8.0 AS final
    WORKDIR /app
    COPY --from=publish /app/publish .
    ENTRYPOINT ["dotnet", "ConsoleApp1.dll"]

    Разве такого файла недостаточно для контейнера без всякого лишнего и необходимости прописывать явно стадии сборки?


    1. VsevKokhan Автор
      20.02.2025 12:05

      Достаточно. Но чтобы все было «более читаемым» и можно было сразу определить на каком именно моменте что то пошло не так - делают примерно как описано в статье. Даже райдер делает автоматический докерфайл примерно таким


    1. withkittens
      20.02.2025 12:05

      Достаточно. dotnet restore отдельным шагом - это просто небольшая оптимизация сборки контейнера, смысл которой в том, что зависимости у вас скорее всего меняются реже, чем код, поэтому их можно закешировать в отдельном слое.

      Причём у автора это реализовано неправильно, потому что он перед рестором копирует все файлы (обратите внимание на COPY . .), инвалидируя этим самым кеш с зависимостями. Лучше обратиться к доке Microsoft.


      1. VsevKokhan Автор
        20.02.2025 12:05

        Подскажите подробнее, что вас именно смутило? Просто я копирую все что связано со сборкой. Нугет пакеты после рестора уйдут в свою папку, которую я не трогаю


        1. alhimik45
          20.02.2025 12:05

          Слой кешируется по хешу, который зависит от всех файлов. Поменяли букву в Program.cs - хеши всех последующих слоёв после COPY . . поменялись, а значит restore будет запущен заново, etc.


  1. dskibin
    20.02.2025 12:05

    НЛО прилетело и "опубликовало" статью?


  1. Maksclub
    20.02.2025 12:05

    Так забавно, что вчера в свою песочницу https://codiew.io/ide прикручивал net 6 (не знал про 8, спасибо) и проделал по сути ваш урок, но с чатом гпт за часик.

    Код билда и запуска (в формате моего окружения): https://github.com/codiewio/codenire/blob/main/sandbox/dockerfiles/net_6/config.json

    Код докерфайла: https://github.com/codiewio/codenire/blob/main/sandbox/dockerfiles/net_6/Dockerfile

    Надо будет по вашим гайдам немного упростить и учесть, что код может с этим xml прилетать, а может без и давать дефолтный.

    Самое печальное, что докерные контейнеры с дотнетом не взлетели в докерном рантайме gvisor от гугла, и этот вопрос требует отдельного изучения, тк пока не работает из включенных языков песочницы он и свежий 23 C++


  1. Maksclub
    20.02.2025 12:05

    Кстати, есть ли какие-то читы в плане того, чтобы собирать быстрее (например заранее системные либы скопировать или проигнорировать какие-то шаги)? Для сандбокса такого рода штуки очень нужны


    1. Madfisht3
      20.02.2025 12:05

      есть чит - монтирование папки кеша нугет пакетов.