Для Python создание образа не сильно отличается от JavaScript, а вот для C# есть принциальное отличие. Дело в том, что Python и JavaScript интерпретируемые языки и им для работы нужен интерпретатор, а C# компилируемый и после сборки проекта .NET в образе уже не нужен. Поэтому для таких образов делают двухстадийную сборку, сначала в образе с установленным .NET компилируют исполняемый файл, а потом делают легковесный образ только с исполняемым файлом, полученным на первой стадии.
Я буду создавать проект на Macos, поэтому использую VSC и консольные команды, но все то же самое можно сделать и в VS
Создание "рыбы" проекта:
dotnet new webapi -minimal -o api_cs
Параметр -o api_cs задает имя создаваемого проекта
Откройте файл Program.cs и поменяйте содержимое по умолчанию на следующее:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () => "Hello World!");
app.Run();
Перейдите в каталог с проектом и запустите команды
dotnet build
dotnet run
На данном этапе проект должен собираться и запускаться без проблем, проверить работу можно выполнив http запрос (порт генерируется автоматически, у вас будет другой):
Используем знакомые вам MySqlConnector и Dapper, установив их соответствующими командами:
dotnet add package MySqlConnector
dotnet add package Dapper
Создайте каталог model и в нем класс Genre (консольная команда: dotnet new class -n Genre)
public class Genre
{
public int id { get; set; }
public required string title { get; set; }
}
string connectionString = "Server=127.0.0.1; Port=3308; User ID=root; Password=toor; Database=mydb";
app.MapGet("/genre", () =>
{
using (MySqlConnection db = new MySqlConnection(connectionString))
{
return db.Query<Genre>(
"SELECT * FROM Genre");
}
});
На этом этапе тоже проблем быть не должно - у меня все запустилось
Для C# существует много систем для миграции:
migrations, где будем хранить скрипты для миграцииВ настройках проекта (файл *.csproj) укажем, чтобы все SQL-файлы из этого каталога включались в проект
<ItemGroup>
<EmbeddedResource Include="migrations/*.sql" />
</ItemGroup>
Создадим файл для миграции migrations/init.sql
insert into Genre values
(1, "Комедия"),
(2, "Ужасы"),
(3, "Триллер");
Устанавливаем библиотеку DBup
dotnet add package dbup-mysql
Дописываем в начало нашего Program.cs код для накатывания миграций
// проверяет наличие возможности подключения к БД
EnsureDatabase.For.MySqlDatabase(connectionString);
// подгатавливает миграцию
// куда и что будем накатывать
var upgrader = DeployChanges.To
.MySqlDatabase(connectionString)
.WithScriptsEmbeddedInAssembly(Assembly.GetExecutingAssembly())
.LogToConsole()
.Build();
// запуск миграции
var result = upgrader.PerformUpgrade();
// проверка результата миграции
if (!result.Successful)
{
throw new Exception("Не смог сделать миграцию");
}
Если все сделали правильно, то в консоли увидите примерно такие логи
2025-09-18 10:31:43 +03:00 [INF] Beginning database upgrade
2025-09-18 10:31:43 +03:00 [INF] Checking whether journal table exists
2025-09-18 10:31:43 +03:00 [INF] Journal table does not exist
2025-09-18 10:31:43 +03:00 [INF] Executing Database Server script 'api_cs.migrations.init.sql'
2025-09-18 10:31:43 +03:00 [INF] Checking whether journal table exists
2025-09-18 10:31:43 +03:00 [INF] Creating the `schemaversions` table
2025-09-18 10:31:44 +03:00 [INF] The `schemaversions` table has been created
2025-09-18 10:31:44 +03:00 [INF] Upgrade successful
А в базе добавится таблица shemaversions, в которой хранится какие миграции и когда применялись (чтобы повторно не накатить уже выполненную миграцию)
Наш проект готов к упаковке в контейнер
Смысл многостадийной сборки в том, что для сборки (компиляции) проекта нужен .NET SDK, а для выполнения уже скомпилированного приложения достаточно так называемого Runtime (но можно пойти еще дальше и сделать Standalone приложение)
SDK - Используется для разработки приложений.
Включает в себя все необходимые инструменты для написания компиляции и сборки приложений:
dotnet build, dotnet run..NET Runtime - Используется для выполнения уже готовых (скомпилированных) приложений.
Включает только библиотеки и компоненты необходимые для работы приложения.
.NET Runtime выгоден, когда на компьютере запускается несколько программ на .NET, но принцип контейнеризации в том, что в каждом контейнере запущено только одно приложение. Компилятор позволяет создавать так называемые Standalone (независимые) приложения, для работы которых фреймворк уже не нужен
Попробуем собрать все три варианта и сравнить размеры полученных образов (взято отсюда)
Ссылки на официальные образы можно посмотреть на сайте Microsoft
Создадим файл Dockerfile.sdk (обратите внимание, имя файла с расширением - мы напишем несколько вариантов):
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
^^^^^^^^
ARG BUILD_CONFIGURATION=Release
WORKDIR /src
COPY api_cs.csproj .
RUN dotnet restore api_cs.csproj
COPY . .
RUN dotnet build api_cs.csproj -c $BUILD_CONFIGURATION -o ./build
RUN dotnet publish api_cs.csproj -c $BUILD_CONFIGURATION -o ./publish /p:UseAppHost=false
CMD ["dotnet", "/src/publish/api_cs.dll"]
что нового:
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build - при указании базового образа .NET мы указали для него алиас build - по этому алиасу мы потом сможем обращаться к содержимому этой стадии сборкиARG BUILD_CONFIGURATION=Release - ARGument позволяет задать константы для часто используемых литераловRUN dotnet restore api_cs.csproj - команда dotnet restore скачивает зависимости проектаRUN dotnet publish api_cs.csproj -c $BUILD_CONFIGURATION -o ./publish /p:UseAppHost=false - Команда dotnet publish компилирует ваше приложение .NET и создает набор файлов (исполняемый файл, сборки, зависимости, файлы конфигурации) в выходной папке для его последующего развертывания на другом компьютере или в другой среде выполнения. Она готовит приложение к распространению и запуску вне среды разработки, собирая все необходимые для этого компоненты.Перед сборкой я вспомнил, что у нас параметры подключения к базе данных прибиты гвоздями - сделаем получение из переменной среды окружения:
string? connectionString = Environment.GetEnvironmentVariable("CONNECTION_STRING");
Console.WriteLine("connectionString (from ENV): {0}", connectionString);
if (connectionString == null)
connectionString = "Server=127.0.0.1; Port=3308; User ID=root; Password=toor; Database=mydb;";
И соберем образ
docker build -f ./Dockerfile.sdk -t api_cs:sdk .
-f ./Dockerfile.sdk - опция -f явно указывает имя докерфайлаЗапускаем собранный образ:
docker run --name test_cs_sdk --network test-network -d -p 5000:8080 -e CONNECTION_STRING="Server=test_mysql; User ID=root; Password=toor; Database=mydb;" api_cs:sdk
Замечания:
8080 хотя в конфигах этого нигде нет - этот порт используется по-умолчанияю для режима Release--network test-networkУ меня все заработало, проверить можно запросив список жанров на 5000 порту:
GET http://localhost:5000/genre
Сборка проекта здесь происходит как и в первом варианте с использованием образа SDK, поэтому скопируем предыдущий докерфайл и дадим название Dockerfile.runtime
Но в конец нового докерфайла допишем второй этап - копирование скомпилированных бинарников из первого образа в базовый образ aspnet:
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
ARG BUILD_CONFIGURATION=Release
WORKDIR /src
COPY api_cs.csproj .
RUN dotnet restore api_cs.csproj
COPY . .
RUN dotnet build api_cs.csproj -c $BUILD_CONFIGURATION -o ./build
RUN dotnet publish api_cs.csproj -c $BUILD_CONFIGURATION -o ./publish /p:UseAppHost=false
FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS runtime
WORKDIR /app
COPY --from=build /src/publish .
CMD ["dotnet", "/app/api_cs.dll"]
В принципе тут все очевидно, в команде COPY добавился параметр --from, который указывает, что исходные данные надо брать не в файловой системе, а в именованом образе
Собираем образ, присвоив ему соответствующий тег
docker build -f ./Dockerfile.runtime -t api_cs:runtime .
Запускаем собранный образ:
docker run --name test_cs_runtime --network test-network -d -p 5001:8080 -e CONNECTION_STRING="Server=test_mysql; User ID=root; Password=toor; Database=mydb;" api_cs:runtime
Все должно работать
GET http://localhost:5001/genre
У меня почему-то не скачивается образ чистого линукс (
FROM alpine), поэтому собрал с образом aspnet (runtime)
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
ARG BUILD_CONFIGURATION=Release
WORKDIR /src
COPY api_cs.csproj .
RUN dotnet restore api_cs.csproj
COPY . .
RUN dotnet build api_cs.csproj -c $BUILD_CONFIGURATION -o ./build
RUN dotnet publish api_cs.csproj -c $BUILD_CONFIGURATION -o ./publish -r linux-arm64 --self-contained true
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS runtime
WORKDIR /app
COPY --from=build /src/publish .
RUN chmod 777 ./api_cs
CMD ./api_cs
В команде dotnet publish добавились параметры
-r linux-arm64 - целевая операционная система и архитектура исполняемого файла (OS в контейнере всегда linux, а вот архитектура зависит от процессора - для Intel нужно указывать linux-x64, в примере вариант для Macos)--self-contained true - собрать standalone приложение (включить все зависимости в приложение)RUN chmod 777 ./api_cs - исполняемому файлу даем права на выполнение и при запуске (CMD ./api_cs) уже не надо писать dotnet
docker build -f ./Dockerfile.standalone -t api_cs:standalone .
docker run --name test_cs_standalone --network test-network -d -p 5002:8080 -e CONNECTION_STRING="Server=test_mysql; User ID=root; Password=toor; Database=mydb;" api_cs:standalone
GET http://localhost:5002/genre
Посмотрим, как отличаются размеры разных вариантов сборки (команда docker images):
REPOSITORY TAG IMAGE ID CREATED SIZE
api_cs runtime bddcceb05493 43 minutes ago 360MB
^^^^^^^ ^^^^^
api_cs sdk 1a233bdb3e4e About an hour ago 1.52GB
^^^ ^^^^^^
api_cs standalone 4e44052e2b01 53 seconds ago 521MB
^^^^^^^^^^ ^^^^^
test-api latest 3b986ccd7bbf 2 weeks ago 254MB
test-mysql latest 72f91044024b 2 weeks ago 800MB
Видно, что runtime занимает в четыре раза меньше места чем sdk.
Использование standalone проверить не смог, образ alpine не скачивается. Удалось скачать образ scratch, образ на его основеучше занимает 160Мб, но приложение в нем не запускается...