Serilog - популярная система логирования для ASP.Net Core (MVC) приложений, которая легко подключается и имеет гибкие настройки. При запуске в Docker (и далее в Kubernetes) важно, чтобы система логирования записывала вывод в stdout, чтобы к ним имели доступ стандартные команды, напр.
docker logs {container_name}
Хорошая новость - при использовании обычного ConsoleSink (вывод в консоль), логи автоматически становятся доступными для docker logs. Не очень хорошая новость - если вы также используете FileSink (вывод в файлы) и хотите хранить эти файлы на хосте,чтобы после перезапуска контейнера они не потерялись, то придется повозиться. Точнее, возможно придется - если вы следуете рекомендациям по безопасности и запускаете процесс внутри контейнера не под root-ом (почему не стоит запускать процессы внутри контейнеров под root, объяснять не буду - об этом и так уже много написано). Но обо всем по порядку.
Для подключения Serilog в приложение ASP.NET MVC Core нам потребуется nuget пакет:
Install-Package Serilog.AspNetCore
Далее в Program.cs включаем логирование:
var builder = WebApplication.CreateBuilder(args); // add serilog builder.Logging.ClearProviders(); builder.Host .UseSerilog((hostingContext, configBuilder) => { configBuilder.ReadFrom.Configuration(hostingContext.Configuration).Enrich.FromLogContext(); });
и добавляем конфигурацию в app.settings, которая может выглядет например так:
{ "Serilog": { "MinimumLevel": { "Default": "Debug", "Override": { "Microsoft": "Information", "System": "Warning" } }, "WriteTo": [ { "Name": "Console", "Args": { "theme": "Serilog.Sinks.SystemConsole.Themes.AnsiConsoleTheme::Code, Serilog.Sinks.Console", "outputTemplate": "{Timestamp:HH:mm:ss}|{RequestId}|{SourceContext}|{Level:u3}|{Message:lj}{NewLine}{Exception}" } }, { "Name": "File", "Args": { "path": "App_Data/logs/log.txt", "rollingInterval": "Day", "retainedFileCountLimit": 31, "outputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss.ffff}|{RequestId}|{SourceContext}|{Level:u3}|{Message:lj}{NewLine}{Exception}", "restrictedToMinimumLevel": "Information" } } ] } }
Здесь мы определяем минимальный уровень для логирования (Debug) и два источника для записи логов: консоль и файл. Файлы будем записывать в App_Data/logs/log.txt с роллинг-интервалом в 1 день (каждый день будет создаваться новый файл с именем log{yyyyMMdd}.txt), хранить на диске будем 31 последний файл (система будет автоматически удалять файлы, старше 1 мес).
Все что осталось сделать - использовать ILogger в нашем коде:
using Microsoft.AspNetCore.Mvc; public class HomeController : Controller { private readonly ILogger<HomeController> logger; public HomeController(ILogger<HomeController> logger) { this.logger = logger; } public IActionResult Index() { this.logger.LogInformation("Visit home page"); return View(); } }
С минимумом усилий и настроек мы получили рабочую систему логирования.
Теперь упакуем наше приложение в Docker. Как я уже упоминал выше, запускать приложение будем под низкопривилегированным пользователем (не под root), которого создадим там же в Docker файле:
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build COPY . /src RUN dotnet restore "/src/MyApp.csproj" RUN dotnet publish "/src/MyApp.csproj" -c Release -o /app/publish /p:UseAppHost=false FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS final RUN apt-get update && apt-get install -y adduser WORKDIR /app ENV ASPNETCORE_URLS=http://+:8080 ENV COMPlus_EnableDiagnostics=0 COPY --from=build /app/publish . RUN addgroup --group my-group --gid 2001 \ && adduser --uid 2000 --gid 2001 my-user \ && chown -R 2000:2001 /app # run as non-root user USER 2000:2001 ENTRYPOINT ["dotnet", "MyApp.dll"]
Здесь мы собираем проект, копируем бинарники в папку /app внутри контейнера, создаем группу (my-group, gid=2001) с пользователем (my-user, uid=2000), которым даем доступ к папке /app, и запускаем наше приложение под созданным пользователем.
Запустим контейнер и проверем, что процесс внутри него действительно работает под созданным пользователем. Подключимся к контейнеру через shell и проверим текущего пользователя и запущенные процессы
> docker exec -it {MyContainer} sh $> id uid=2000(my-user) gid=2001(my-group) groups=2001(my-group) $> ps aux USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND my_user 1 0.0 3.4 274066700 70844 ? Ssl Sep21 5:17 dotnet MyApp.dll ...
Как видим, процесс действительно работает под созданным пользователем, а не под root-ом. Там же (внутри контейнера) проверим, что логи успешно записываются в файл:
$> ls /app/App_Data/logs ... log20261001.txt $> more /app/App_Data/logs/log20261001.txt 2026-10-01 14:40:03.0288||Microsoft.AspNetCore.Server.Kestrel|WRN|As of ""2026-10-01T14:40:02.9559252+00:00"", the heartbeat has been running for ""00:00:01.38 37155"" which is longer than ""00:00:01"". This could be caused by thread pool starvation.
Как видим, логи успешно сохраняются в файлы. Забегая вперед (чтобы два раза не вставать), проверим права на папку /app/App_Data/logs:
$> ls -ld /app/App_Data/logs drwxr-xr-x 2 my-user my-group 4096 Oct 1 14:40 /app/App_Data/logs
Как и ожидалось, папка принадлежит нашему пользователю (2000:2001).
Логи сохраняются, но у текущего подхода есть серьезный недостаток: они сохраняются внутри контейнера, т.е. перезапуск контейнера приведет к потере логов. Нас такое положение дел не устраивает. Самое простое решение - смапировать папку /app/App_Data/logs на локальный том, чтобы логи сохранялись в папку на хосте (в этом примере будем использовать папку /var/log/MyApp):
services: web: image: MyImage build: context: ../ dockerfile: ./MyApp/Dockerfile volumes: - /var/log/MyApp:/app/App_Data/logs
Перезапускаем котейнер, смотрим содержимое папки /app/App_Data/logs - а там пусто:
> docker exec -it {MyContainer} sh $> ls /app/App_Data/logs
Что могло пойти не так? Подсказка есть выше - проверим теперь права на папку /app/App_Data/logs:
$> ls -ld /app/App_Data/logs drwxr-xr-x 2 root root 4096 Oct 2 07:29 /app/App_Data/logs
Причина найдена: как видим, директория теперь принадлежит root:root (права унаследовались с папки хоста), а процесс работает под my-user (uid=2000).
Самый простой вариант для решения проблемы - сменить владельца папки /var/log/MyApp на хосте, используя uid и gid пользователя и группы из нашего Docker файла. Это решение, мягко говоря, неидеально (прежде всего стоит проверить, что на хосте uid=2000 и gid=2001 уже не используются, чтобы случайно не открыть дыру в системе), но для целей демонстрации проблемы в данной статье подойдет. На хосте выполняем:
mkdir -p /var/log/MyApp sudo chown -R 2000:2001 /var/log/MyApp
и перезапускаем контейнер. Теперь логи Serilog успешно сохраняются в папку на хосте.
Еще раз повторю, что решение с chown на хосте здесь представлено лишь для целей демонстрации проблемы. Стоит серьезно подумать, прежде чем использовать его в боевом окружении. Другие решения, которые я нашел (напр. на основе ENTRYPOINT и запуска отдельного контейнера для настройки прав папки) тоже имеют свои недостатки. Если у вас есть проверенные способы решения описанной проблемы, просьба поделиться опытом в комментариях.