<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Graylog on rHomelab</title><link>https://rhomelab.com/tags/graylog/</link><description>Recent content in Graylog on rHomelab</description><generator>Hugo</generator><language>en-US</language><lastBuildDate>Thu, 08 Oct 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://rhomelab.com/tags/graylog/index.xml" rel="self" type="application/rss+xml"/><item><title>Centralized Logging for Your Homelab: Loki vs Graylog vs the ELK Stack</title><link>https://rhomelab.com/self-hosted/self-hosted-log-aggregation-loki-vs-graylog/</link><pubDate>Thu, 08 Oct 2026 00:00:00 +0000</pubDate><guid>https://rhomelab.com/self-hosted/self-hosted-log-aggregation-loki-vs-graylog/</guid><description>Pulling scattered container and host logs into one searchable place with Loki, Graylog, or the ELK stack, and picking the right one for how much log volume your homelab actually produces.</description><content:encoded><![CDATA[<p>Metrics tell you a service is dying. Logs tell you why. If you&rsquo;ve ever SSH&rsquo;d into three different containers in a row trying to figure out which one actually threw the error that broke your reverse proxy, you already know the gap that centralized logging fills. Uptime checks and Grafana dashboards (covered in the <a href="/self-hosted/self-hosted-monitoring-stack/">monitoring stack guide</a>) tell you something is wrong and roughly when. Logs tell you what the service was actually doing in the seconds before it went wrong, and without them you&rsquo;re stuck doing <code>docker logs</code> on one container at a time, hoping you guessed right on the first try.</p>
<p>The homelab version of this problem is smaller than what these tools were built for, but the pain is identical: logs scattered across a dozen containers and a couple of hosts, no shared timeline, and no way to search across all of them at once when something breaks at 2am and you need an answer fast.</p>
<h2 id="what-centralized-logging-actually-buys-you">What centralized logging actually buys you</h2>
<p>Three things, in order of how often you&rsquo;ll actually use them:</p>
<ol>
<li><strong>One search box across everything.</strong> Grep a hostname, a request ID, or an error string and get results from every container and host that logged it, in time order, instead of opening a terminal per service.</li>
<li><strong>Correlation across services.</strong> A failed login on your auth service and a 500 from the app behind it, in the same timeline, make root cause obvious instead of requiring you to mentally merge two separate log streams.</li>
<li><strong>Retention past what the container runtime keeps.</strong> Docker&rsquo;s default logging driver rotates logs and throws old ones away. Once a container restarts, whatever was in its logs before that is usually gone unless you shipped it somewhere else first.</li>
</ol>
<p>None of this prevents an outage. It cuts the time between &ldquo;something&rsquo;s wrong&rdquo; and &ldquo;I know what broke it&rdquo; from twenty minutes of log-spelunking to a two-minute search.</p>
<h2 id="the-three-real-options-and-who-theyre-actually-for">The three real options, and who they&rsquo;re actually for</h2>
<p><strong>Grafana Loki</strong> is the lightest of the three and the one most homelabs should start with, especially if Grafana is already running for metrics. Loki doesn&rsquo;t index log content the way the other two do, it indexes labels (container name, host, job) and stores the raw log lines compressed alongside them. That tradeoff means much lower resource usage and storage cost, at the price of slower full-text search across huge volumes - a tradeoff that&rsquo;s irrelevant at homelab scale, where you&rsquo;re searching gigabytes, not terabytes, per day.</p>
<p><strong>Graylog</strong> sits in the middle. It&rsquo;s built on Elasticsearch/OpenSearch under the hood but wraps it in a purpose-built log-management UI with saved searches, dashboards, and alerting rules that are easier to reach for than wiring up Kibana from scratch. It wants more RAM than Loki (realistically 4GB+ for the stack, more if retention grows), but the UI is more approachable for someone who wants a real log-management tool rather than a Grafana plugin.</p>
<p><strong>The ELK/Elastic stack</strong> (Elasticsearch, Logstash, Kibana, or OpenSearch&rsquo;s fork of the same shape) is the heaviest and most capable option, and genuinely more than a single-operator homelab needs unless you&rsquo;re specifically trying to learn it for work. Elasticsearch alone wants multiple gigabytes of heap just to idle comfortably, Logstash&rsquo;s pipeline configuration has a real learning curve, and the thing you get at the end is enterprise-grade full-text search and analytics that a homelab&rsquo;s log volume will never come close to stressing. If you&rsquo;re running ELK at work and want the same tool at home for muscle memory, that&rsquo;s a legitimate reason. If you just want to find out why a container crashed, it&rsquo;s the wrong tool for the job size.</p>
<p>For almost every homelab, the real choice is Loki if Grafana&rsquo;s already in the stack, Graylog if you want a dedicated log UI without touching Elasticsearch configuration directly.</p>
<h2 id="setting-up-loki-the-path-most-homelabs-should-take">Setting up Loki (the path most homelabs should take)</h2>
<p>If Prometheus and Grafana are already running per the monitoring guide, Loki slots in as a third piece feeding the same Grafana frontend you&rsquo;re already using. The missing piece is <strong>Promtail</strong> (or its newer replacement, <strong>Alloy</strong>), the agent that tails container logs and ships them to Loki with the right labels attached.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">services</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">loki</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">grafana/loki:latest</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">./loki-data:/loki</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">ports</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;3100:3100&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">restart</span><span class="p">:</span><span class="w"> </span><span class="l">unless-stopped</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">promtail</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">grafana/promtail:latest</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">/var/lib/docker/containers:/var/lib/docker/containers:ro</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">/var/run/docker.sock:/var/run/docker.sock:ro</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">./promtail-config.yml:/etc/promtail/config.yml</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">command</span><span class="p">:</span><span class="w"> </span>-<span class="l">config.file=/etc/promtail/config.yml</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">restart</span><span class="p">:</span><span class="w"> </span><span class="l">unless-stopped</span><span class="w">
</span></span></span></code></pre></div><p>A minimal <code>promtail-config.yml</code> using Docker service discovery, so every container gets picked up automatically instead of hand-listing them:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">server</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">http_listen_port</span><span class="p">:</span><span class="w"> </span><span class="m">9080</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">positions</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">filename</span><span class="p">:</span><span class="w"> </span><span class="l">/tmp/positions.yaml</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">clients</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">url</span><span class="p">:</span><span class="w"> </span><span class="l">http://loki:3100/loki/api/v1/push</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">scrape_configs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">job_name</span><span class="p">:</span><span class="w"> </span><span class="l">docker</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">docker_sd_configs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">host</span><span class="p">:</span><span class="w"> </span><span class="l">unix:///var/run/docker.sock</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">refresh_interval</span><span class="p">:</span><span class="w"> </span><span class="l">15s</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">relabel_configs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">source_labels</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s1">&#39;__meta_docker_container_name&#39;</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">target_label</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;container&#39;</span><span class="w">
</span></span></span></code></pre></div><p>Add Loki as a data source in Grafana (<code>http://loki:3100</code>) and query it through the Explore tab using LogQL, Loki&rsquo;s query language. It reads a lot like a filtered grep: <code>{container=&quot;vaultwarden&quot;} |= &quot;error&quot;</code> pulls every log line from that container containing &ldquo;error.&rdquo; Pin a saved query for the services you check most often, same as you&rsquo;d pin a dashboard.</p>
<p>If you&rsquo;re running this inside an LXC rather than a VM, the same Docker-in-LXC networking gotcha from the monitoring guide applies here too - <code>network_mode: host</code> is the standard fix if containers can&rsquo;t reach each other over the default bridge.</p>
<h2 id="setting-up-graylog-if-you-want-the-dedicated-ui-instead">Setting up Graylog, if you want the dedicated UI instead</h2>
<p>Graylog needs MongoDB (for configuration) and OpenSearch (for log storage) alongside itself, so it&rsquo;s a heavier compose stack than Loki out of the gate:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">services</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">mongodb</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">mongo:6</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">./mongo-data:/data/db</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">restart</span><span class="p">:</span><span class="w"> </span><span class="l">unless-stopped</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">opensearch</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">opensearchproject/opensearch:2</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">environment</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;discovery.type=single-node&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;OPENSEARCH_JAVA_OPTS=-Xms1g -Xmx1g&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;DISABLE_SECURITY_PLUGIN=true&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">./opensearch-data:/usr/share/opensearch/data</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">restart</span><span class="p">:</span><span class="w"> </span><span class="l">unless-stopped</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">graylog</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">graylog/graylog:6.0</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">environment</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">GRAYLOG_PASSWORD_SECRET=change-this-to-a-long-random-string</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">GRAYLOG_ROOT_PASSWORD_SHA2=sha256-hash-of-your-admin-password</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">GRAYLOG_HTTP_EXTERNAL_URI=http://graylog.yourdomain.lan/</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">depends_on</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">mongodb</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">opensearch</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">ports</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;9000:9000&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;5140:5140/udp&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">restart</span><span class="p">:</span><span class="w"> </span><span class="l">unless-stopped</span><span class="w">
</span></span></span></code></pre></div><p><code>GRAYLOG_ROOT_PASSWORD_SHA2</code> wants the SHA-256 hash of your chosen password, not the password itself - generate it with <code>echo -n 'yourpassword' | sha256sum</code> and paste the hash in.</p>
<p>Once it&rsquo;s up, the web UI walks you through creating an <strong>Input</strong> - the listener that actually receives logs. GELF UDP is the lightest option for Docker containers; point your containers&rsquo; logging driver at it, or run a small sidecar like <code>gelf-docker</code> that forwards from the Docker socket the way Promtail does for Loki. From there, Graylog&rsquo;s own search UI (closer to a purpose-built log tool than Grafana&rsquo;s Explore tab) handles the querying, saved searches, and alert rules.</p>
<h2 id="making-docker-actually-ship-its-logs">Making Docker actually ship its logs</h2>
<p>Neither tool does anything until containers are configured to send logs somewhere other than Docker&rsquo;s default <code>json-file</code> driver sitting on local disk. Two paths:</p>
<ul>
<li><strong>Sidecar/agent pattern</strong> (what both examples above use): leave each container&rsquo;s logging driver alone and let Promtail or a GELF forwarder read from the Docker socket and ship logs out. Simplest to retrofit onto an existing stack with no per-service changes.</li>
<li><strong>Native driver pattern</strong>: set each container&rsquo;s <code>logging</code> block directly, e.g. <code>driver: gelf</code> with <code>gelf-address: udp://graylog-host:5140</code>. More explicit and avoids the sidecar entirely, but means touching every compose file instead of adding one new service.</li>
</ul>
<p>For a homelab with a dozen-plus containers already running, the sidecar pattern is almost always less work - one new service picks up everything already running, instead of editing every existing one.</p>
<h2 id="retention-the-setting-that-actually-needs-a-decision">Retention: the setting that actually needs a decision</h2>
<p>Unlike metrics, where a few weeks of history is plenty, logs grow fast and most of them you&rsquo;ll never read. Decide retention deliberately instead of letting the default run until the disk fills:</p>
<ul>
<li><strong>Loki</strong> retention is set in its config (<code>limits_config.retention_period</code>) or via a <code>compactor</code> with retention enabled - without one of those set, Loki keeps everything forever by default.</li>
<li><strong>Graylog/OpenSearch</strong> retention is managed through Index Set rotation and retention settings in the web UI - rotate daily or weekly, keep a fixed number of indices, and older ones get deleted automatically.</li>
</ul>
<p>Seven to fourteen days is plenty for a homelab. You&rsquo;re using this to debug something that just happened, not building a compliance audit trail. If a specific incident needs longer-term evidence, export that one log window before it rotates out rather than keeping everything indefinitely by default.</p>
<h2 id="what-to-actually-run">What to actually run</h2>
<p>If Grafana&rsquo;s already part of the stack, Loki is close to free to add and keeps everything in one pane of glass with the metrics dashboards already there. If logging feels like it deserves its own dedicated tool with a purpose-built search UI and you&rsquo;ve got the RAM to spare, Graylog is the more complete answer without the full weight of hand-rolled Elasticsearch and Logstash. Skip ELK/Elastic entirely unless there&rsquo;s a specific reason (usually: matching a work stack) to take on the heaviest option for a job a homelab&rsquo;s log volume doesn&rsquo;t actually require.</p>
<p>Either way, the real win isn&rsquo;t the dashboard, it&rsquo;s the next time something breaks and the fix is a thirty-second search instead of a guessing tour through <code>docker logs</code> on every container that might be the culprit.</p>
]]></content:encoded></item></channel></rss>