ASP.NET Core: сохраняем логи Serilog из контейнера Docker на хосте

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 и запуска отдельного контейнера для настройки прав папки) тоже имеют свои недостатки. Если у вас есть проверенные способы решения описанной проблемы, просьба поделиться опытом в комментариях.
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.