Homeserver logging setup

Oct 10, 2026

After tediously removing logs because of a full disk many times, I started looking into log collection, rotation and analysis solutions for servers. After not so long i found syslog-ng to be a nice fit for my setup. It even has a discord drain!

Working it out

Grafana this, Prometheus that. You need Loki my man. These are some enterprise grade solutions which even for enterprise maybe are not the right choice. Not long into reading about syslog-ng I found that it is a quite configurable piece of software with its own DSL. It even has threading support for each logging pipe you create. So without further ado, let me present to you the way i set it up for my stuff!

As may be known (or not) i run multiple services on my homeserver, which generate tons of logs which fill up my disk with (mostly) garbage. One method is to just keep them in RAM and drop them afterwards the other is to persist them somewhere and rotate them.

The plan

Some tinkering and reading about the capabilities of syslog-ng a picture was made using plantuml, behold: logging_setup Where docker.log and everything.log are stored in tmpfs.

Configuration

Docker

First and foremost (as most of my services are running in docker) i added the following configuration to /etc/docker/daemon.json:

{
  "log-driver": "syslog",
  "log-opts": {
    "syslog-address": "unixgram:///var/log/syslog-ng.sock",
    "syslog-facility": "local7",
    "syslog-format": "rfc5424",
    "tag": "{{.ImageName}};{{.Name}}"
  }
}

syslog-ng

Now to the core of the configuration. This is now a lot of regex work, to clean up the output of the different docker services, because some output the service names themselves, some don’t and it was a bit tedious. So advice to you would be to learn regex.

  1. Sources configuration:
source src {
	system();
	internal();
	program("sshd");
};

source s_unix {
	unix-dgram("/var/log/syslog-ng.sock", flags(syslog-protocol));
};
  1. The duplicate cleaner (warning: a regex sausage):
rewrite r_docker_cleaner {
	subst( '(\[[0-9]{2}\/[A-Za-z]{3}\/[0-9]{4}:[0-9]{2}:[0-9]{2}:[0-9]{2} \+[0-9]{4}\]) (?P<message>.*)' "${message}", value("MESSAGE") );
	subst( "([0-9]{4}\/[0-9]{2}\/[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}|[a-zA-Z]{3} [0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}|[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]{6}\+[0-9]{2}:[0-9]{2}) (?P<message>.*)" "${message}", value("MESSAGE") );
};
  1. Now to bind these sources and pipe them through the cleaner, this is done using a channel:
source sc_docker_cleaner {
	channel {
		source (s_unix);
		rewrite(r_docker_cleaner);
	};
};
  1. Destinations (notice the logrotate directive):
destination d_checkthis  { file("/var/log/checkthis.log"); };
destination d_everything { file("/var/log/everything.log" logrotate(enable(yes), size(1G), rotations(1))); };
destination d_kernel     { file("/var/log/kernel.log"); };
destination d_auth       { file("/var/log/auth.log"); };
destination d_docker     { file("/var/log/docker.log" logrotate(enable(yes), size(1G), rotations(1))); };
destination d_mvalheim   { discord( url("https://discord.com/api/webhooks/REDACTED") username("REDACTED") template("${MESSAGE:-[empty message]}") ); };
destination d_mfactorio  { discord( url("https://discord.com/api/webhooks/REDACTED") username("REDACTED") template("${MESSAGE:-[empty message]}") ); };
  1. Filters for Discord to only log interesting information, the filter directive takes messages that are to be then rewritten using the rewrite directive into human readable format (shortened to not show regexes):
filter  f_mvalheim_up { match(""); };
rewrite r_mvalheim_up { "Join server with ${JOINCODE}." };
filter  f_mvalheim_message { match(""); };
rewrite r_mvalheim_message { "${USER} said ${MESSAGE}." };
filter  f_mfactorio_join  { match(""); };
rewrite r_mfactorio_join  { "${USER} started automating." };
filter  f_mfactorio_leave { match(""); };
rewrite r_mfactorio_leave { "${USER} left to do chores." };
filter  f_mfactorio_chat  { match(""); };
rewrite r_mfactorio_chat  { "${USER}: ${MESSAGE}" };
  1. The log directives, they propagate sources to destinations with filters and rewrites (imagine pipes):
log { source(src); source(sc_docker_cleaner); filter{ level(warn..emerg); }; destination(d_checkthis); };
log { source(src); source(sc_docker_cleaner); filter{ facility(kern); }; destination(d_kernel); };
log { source(src); source(sc_docker_cleaner); filter{ facility(4) or facility(10) or facility(13); }; destination(d_auth); };
log { source(src); source(sc_docker_cleaner); filter(f_auth_fail); destination(d_auth); };

log { source(src); source(sc_docker_cleaner); destination(d_everything); };
log { source(sc_docker_cleaner); destination(d_docker); };

log { source(sc_docker_cleaner); filter(f_mvalheim_up); rewrite(r_mvalheim_up); destination(d_mvalheim); };
log { source(sc_docker_cleaner); filter(f_mvalheim_message); rewrite(r_mvalheim_message); destination(d_mvalheim); };

log { source(sc_docker_cleaner); filter(f_mfactorio_join); rewrite(r_mfactorio_join); destination(d_mfactorio); };
log { source(sc_docker_cleaner); filter(f_mfactorio_chat); rewrite(r_mfactorio_chat); destination(d_mfactorio); };
log { source(sc_docker_cleaner); filter(f_mfactorio_leave); rewrite(r_mfactorio_leave); destination(d_mfactorio); };