Log Management on a Single Server
Advertisement
When something goes wrong on a single server, the logs are usually all there. They are just scattered across /var/log/nginx/, /var/log/postgresql/, the systemd journal, and whatever custom path the application itself decided to write to.
Finding the relevant line means knowing exactly which of those five places to look. If the app logs in free-text instead of structured JSON, finding "all requests from this IP in the last hour, across Nginx and the app" requires multiple grep commands with entirely different formats. None of this is technically broken. It is just far slower than it needs to be, especially when you are debugging under pressure.
The full ELK stack (Elasticsearch, Logstash, Kibana) absolutely solves this. But it is genuinely too heavy for one server. Elasticsearch alone demands a meaningful chunk of memory just to idle. The useful middle ground for a single server is much smaller than that.
Structured logs make everything else easier
The absolute highest-leverage change you can make is getting application logs into a structured format (JSON) instead of free-text. Compare these two:
2026-10-28 14:32:01 ERROR Failed to process order 4821 for user 1029: timeout
{"time":"2026-10-28T14:32:01Z","level":"error","msg":"failed to process order","order_id":4821,"user_id":1029,"reason":"timeout"}
The second one isn't instantly more readable to a human glancing at a terminal. But it is queryable. "Every error for user 1029" or "every timeout in the last hour" becomes a precise filter on a field, rather than a fuzzy regex that might accidentally match unrelated text containing the same numbers. Most modern logging libraries support structured JSON output with a simple configuration change, no code rewrite required.
journald is already doing more than people use
For anything running as a systemd service (which is practically everything on a modern Linux server), journalctl already centralizes stdout and stderr with strict timestamps. It also supports filtering that a surprisingly large number of admins never reach for:
# Everything from the app, last hour
journalctl -u myapp --since "1 hour ago"
# Follow logs from multiple units at once
journalctl -u myapp -u nginx -f
# Only errors, across everything
journalctl -p err --since today
That last command—filtering by priority across the entire system—is something a scattered collection of .log files literally cannot do without external tooling. If your application logs go directly to stdout in JSON (rather than to a separate file), journalctl's output for that unit is already structured and can be instantly piped into jq for filtering on specific fields.
A lightweight stack, if you want dashboards
If you want something closer to a real log viewer without Elasticsearch's massive footprint, Grafana Loki plus Promtail is the current default recommendation.
Promtail tails log files (or reads the systemd journal directly) and ships them to Loki. Grafana then queries Loki and displays the logs alongside your metrics dashboards from Prometheus. Loki's resource footprint is small enough that it runs comfortably alongside your application on the exact same VPS. That is the main reason it fits here where Elasticsearch absolutely would not.
# promtail-config.yml (minimal)
scrape_configs:
- job_name: journal
journal:
max_age: 12h
relabel_configs:
- source_labels: ['__journal__systemd_unit']
target_label: 'unit'
Don't forget log rotation
None of this matters if your disk quietly fills up to 100% with logs nobody is looking at. logrotate is usually already configured for standard services, but application logs written to a custom path desperately need their own entry:
# /etc/logrotate.d/myapp
/var/log/myapp/*.log {
daily
rotate 14
compress
missingok
notifempty
}
For the systemd journal itself, hard-capping its size avoids the journal silently growing unbounded over several years:
# /etc/systemd/journald.conf
SystemMaxUse=1G
The actual goal
None of this needs to be an elaborate enterprise setup. The entire goal is that when something breaks at 2 AM, the question "what happened, and when?" has exactly one answer-able place to look. You want enough structure to filter by what actually matters (a user ID, an error type, a time range) rather than scrolling through endless plain text hoping the relevant line catches your eye.
Structured logging plus journalctl's built-in filtering covers a lot of that for free. Loki and Grafana just add a browsable interface on top for when "for free" stops being quite enough.
Advertisement