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 и запуска отдельного контейнера для настройки прав папки) тоже имеют свои недостатки. Если у вас есть проверенные способы решения описанной проблемы, просьба поделиться опытом в комментариях.

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