Всем привет! Когда я узнаю, что человек передо мной начинает изучать 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)
alhimik45
20.02.2025 12:05FROM 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"]
Разве такого файла недостаточно для контейнера без всякого лишнего и необходимости прописывать явно стадии сборки?
VsevKokhan Автор
20.02.2025 12:05Достаточно. Но чтобы все было «более читаемым» и можно было сразу определить на каком именно моменте что то пошло не так - делают примерно как описано в статье. Даже райдер делает автоматический докерфайл примерно таким
withkittens
20.02.2025 12:05Достаточно.
dotnet restore
отдельным шагом - это просто небольшая оптимизация сборки контейнера, смысл которой в том, что зависимости у вас скорее всего меняются реже, чем код, поэтому их можно закешировать в отдельном слое.Причём у автора это реализовано неправильно, потому что он перед рестором копирует все файлы (обратите внимание на
COPY . .
), инвалидируя этим самым кеш с зависимостями. Лучше обратиться к доке Microsoft.VsevKokhan Автор
20.02.2025 12:05Подскажите подробнее, что вас именно смутило? Просто я копирую все что связано со сборкой. Нугет пакеты после рестора уйдут в свою папку, которую я не трогаю
alhimik45
20.02.2025 12:05Слой кешируется по хешу, который зависит от всех файлов. Поменяли букву в Program.cs - хеши всех последующих слоёв после COPY . . поменялись, а значит restore будет запущен заново, etc.
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++
serhio_ribeira
если все же оставить команду dotnet restore то у нас останется кеш с нугетами (по-крайней мере если мы используем Canico для сборки докера на CI), не уверен что при dotnet publish скачанные нугеты останутся в кеше
alhimik45
Под кешем имеется в виду закешированный layer контейнера? Но он же инвалидируется из-за `COPY . .` и кроме вырожденных случаев переиспользоваться не будет